Skip to content

Instantly share code, notes, and snippets.

@dzigner
Created February 2, 2026 21:54
Show Gist options
  • Select an option

  • Save dzigner/2e53d5d25d2e5f16a4d3050daea39dc7 to your computer and use it in GitHub Desktop.

Select an option

Save dzigner/2e53d5d25d2e5f16a4d3050daea39dc7 to your computer and use it in GitHub Desktop.
Shehrullah Dashboard
function getAdminData() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
// Stats & Behnos from AvailableCapacity - Filter out empty rows
// Ensure Quotas are fresh before returning stats
const calcResult = forceRecalculate();
const calculatedQuotas = calcResult.quotas || { daily: 0, lf: 0 };
// Re-fetch updated rows after calculation
const fresh = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
let behnos = fresh.rows.filter(r => (r.Email || r.email || '').trim() !== '');
// Sort by HOFIDs (Family Grouping) - Ensure families stay together
// Secondary sort: preserve sheet order (sign up order)
behnos = behnos.map((r, i) => ({...r, _originalIndex: i}));
behnos.sort((a, b) => {
const idA = parseInt(String(a.HOFIDs || a.hofids || '0').replace(/\D/g, '')) || 0;
const idB = parseInt(String(b.HOFIDs || b.hofids || '0').replace(/\D/g, '')) || 0;
// Primary Sort: Group by Family (HOFID)
if (idA !== idB) {
// If one has ID and other doesn't (0), decide priority?
// User Rule: "Families together".
// Simple numeric sort groups non-families (0) at top or bottom.
// Let's keep 0s at the end or treated as their own group?
// Standard integer sort matches HOFIDs.
return idA - idB;
}
// Secondary Sort: Signup Order (Original Index)
// If same HOFID (family members), OR both are 0 (individuals),
// sort by who occurs first in the sheet.
return a._originalIndex - b._originalIndex;
});
// Cleanup temp property
behnos.forEach(b => delete b._originalIndex);
const totalBehnos = behnos.length;
// Use the REAL calculated quotas (Standard) instead of naive averages
const dailyQuota = calculatedQuotas.daily;
const lfQuota = calculatedQuotas.lf;
return {
stats: {
total: totalBehnos,
dailyQuota: dailyQuota,
lfQuota: lfQuota
},
behnos: behnos,
/* ADDED BY ANTIGRAVITY: Including current active phase for UI feedback */
activePhase: PropertiesService.getScriptProperties().getProperty('ACTIVE_PHASE') || '',
phaseSchedule: getPhaseSchedule(), /* ADDED: Include Schedule */
scriptUrl: ScriptApp.getService().getUrl(),
currentUserEmail: Session.getActiveUser().getEmail(), /* ADDED: For navigation persistence */
/* END OF ADDITION */
dailyJagah: getJagahData('Daily'),
lfJagah: getJagahData('LF')
};
}
function getJagahData(type) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const jagahTab = (type === 'Daily') ? TABS.DAILY_JAGAH : TABS.LF_JAGAH;
const jagahSheet = ss.getSheetByName(jagahTab);
if (!jagahSheet) return { seats: [], dates: [], mapping: {} };
const allData = jagahSheet.getDataRange().getDisplayValues();
if (allData.length < 1) return { seats: [], dates: [], mapping: {} };
// 1. Get Seats from Row 1 (Columns B-O, index 1-14)
const headers = allData[0];
const seats = [];
headers.forEach((h, i) => {
if (i > 0 && i <= 14 && String(h).trim()) {
seats.push(String(h).trim());
}
});
// 2. Get Dates from Column A (Filter based on type/phase)
const dates = [];
const startRow = 1;
const endRow = allData.length;
for (let r = startRow; r < endRow; r++) {
const dVal = String(allData[r][0]).trim();
if (dVal && dVal !== 'null') {
dates.push(dVal);
}
}
// 3. Mapping assignments: { "Date|Seat": "Name" }
const mapping = {};
for (let r = 1; r < allData.length; r++) {
const dStr = String(allData[r][0]).trim();
if (!dStr) continue;
seats.forEach((s, idx) => {
// s is seat name, but we need colIndex.
// seatColumns are index 1 to 14.
// The 'seats' array contains names. We can find colIndex.
const colIdx = headers.indexOf(s);
if (colIdx > -1) {
const val = String(allData[r][colIdx]).trim();
if (val) mapping[dStr + "|" + s] = val;
}
});
}
return {
seats: seats,
dates: dates,
mapping: mapping
};
}
function updateAdminValue(email, field, value) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const sheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const data = sheet.getDataRange().getValues();
const headers = data[0];
const emailIdx = headers.indexOf('Email');
if (emailIdx === -1) throw new Error("Email column not found");
const rowIdx = data.slice(1).findIndex(r => String(r[emailIdx]).toLowerCase() === String(email).toLowerCase());
if (rowIdx === -1) throw new Error("User not found");
const colIdx = headers.indexOf(field);
if (colIdx === -1) throw new Error("Column " + field + " not found");
// Update the value
sheet.getRange(rowIdx + 2, colIdx + 1).setValue(value);
// If updating AdminDaily or AdminLF, recalculate quotas for all users
if (field.toLowerCase().includes('admin')) {
recalculateQuotas(sheet, headers);
}
return { success: true };
}
function recalculateQuotas(sheet, headers) {
const allData = sheet.getDataRange().getValues();
const hIdx = findHeaderRow(allData);
const headerRow = allData[hIdx].map(h => String(h).trim().toLowerCase());
const emailIdx = headerRow.indexOf('email');
if (emailIdx === -1) return;
const validRows = []; // { data: row, rowIndex: relativeIndexFromHeader }
// 1. Identify valid rows (skip header)
for (let i = hIdx + 1; i < allData.length; i++) {
const row = allData[i];
if (row[emailIdx] && String(row[emailIdx]).trim() !== '') {
validRows.push({ row: row, index: i });
}
}
if (validRows.length === 0) return;
const findC = (list) => {
for (let t of list) {
let i = headerRow.indexOf(t.toLowerCase());
if (i !== -1) return i;
}
return -1;
};
const dailyQuotaIdx = findC(['DailyQuota', 'QuotaDaily', 'Daily Quota']);
const dailyUsedIdx = findC(['DailyUsed', 'UsedDaily']);
const dailyLeftIdx = findC(['DailyLeft', 'LeftDaily']);
const adminDailyIdx = findC(['DailyAdmin', 'AdminDaily', 'Admin Daily']);
const lfQuotaIdx = findC(['LFQuota', 'QuotaLF', 'LF Quota']);
const lfUsedIdx = findC(['LFUsed', 'UsedLF']);
const lfLeftIdx = findC(['LeftLF', 'LFLeft']);
const adminLFIdx = findC(['LFAdmin', 'AdminLF', 'Admin LF']);
if (dailyQuotaIdx === -1 || lfQuotaIdx === -1) return;
// 1. Fetch Used Assignments
const getUsedMap = (tab) => {
const s = sheet.getParent().getSheetByName(tab);
if (!s) return {};
const d = s.getDataRange().getDisplayValues(); // Display values for safety
const h = (d[0] || []).map(c => String(c).toLowerCase().trim()); // Assume row 1 header for Grids as per Seats.gs
// Find email col in grid? No, grids usually have Name.
// Wait, earlier logic used Name to match. Now we switched to Email?
// "seats.gs" writes UserName to grid.
// So we must match by UserName (Normalised).
// BUT "UserMaster" or "AvailableCapacity" defines the Name.
// Let's assume the name in AvailableCapacity matches what's written to Grid.
const map = {};
for(let r=1; r<d.length; r++) {
for(let c=1; c<d[0].length; c++) {
const val = String(d[r][c]).trim().toLowerCase(); // Normalize check
if(val) map[val] = (map[val] || 0) + 1;
}
}
return map;
};
const dailyUsedMap = getUsedMap(TABS.DAILY_JAGAH);
const lfUsedMap = getUsedMap(TABS.LF_JAGAH);
const totalDailyPots = 14 * 24;
const totalLFPots = 14 * 7;
// 2. Calculate Stats
let totalAdminDaily = 0;
let totalAdminLF = 0;
let usersWithoutAdminDaily = 0;
let usersWithoutAdminLF = 0;
validRows.forEach(item => {
const row = item.row;
const adminDaily = (adminDailyIdx > -1) ? row[adminDailyIdx] : '';
const adminLF = (adminLFIdx > -1) ? row[adminLFIdx] : '';
if (adminDaily !== '' && adminDaily !== null && adminDaily !== undefined) totalAdminDaily += Number(adminDaily);
else usersWithoutAdminDaily++;
if (adminLF !== '' && adminLF !== null && adminLF !== undefined) totalAdminLF += Number(adminLF);
else usersWithoutAdminLF++;
});
/* REMOVED: Duplicate definition that caused SyntaxError.
The constants were already defined at lines 194-195. */
// LOGIC FIX: If everyone has admin quota, don't divide by 0.
// If no one has admin quota, divide total by count.
// If count is 0? (No eligible users).
let adjustedDailyQuota = 0;
if (usersWithoutAdminDaily > 0) {
const availablePool = Math.max(0, totalDailyPots - totalAdminDaily);
adjustedDailyQuota = Math.floor(availablePool / usersWithoutAdminDaily);
}
let adjustedLFQuota = 0;
if (usersWithoutAdminLF > 0) {
const availablePool = Math.max(0, totalLFPots - totalAdminLF);
adjustedLFQuota = Math.floor(availablePool / usersWithoutAdminLF);
}
// Safeguard: Ensure at least 1 if pots available and user count low?
// No, strict math is better.
Logger.log(`Recalc: Users=${validRows.length}, NoAdminDaily=${usersWithoutAdminDaily}, AdjDaily=${adjustedDailyQuota}`);
// 3. Prepare Updates
// We will update the sheet row by row to be safe with disjointed rows,
// OR we can read the whole range and write back whole range.
// Writing back whole range is better but might overwrite concurrent edits if not careful.
// Given we are recalculating logic columns, let's write back logic columns only? No, bulk read/write all is easiest.
// Actually, 'allData' is already read. We can update 'allData' in memory and write back.
// BUT validRows only modifies specific rows.
// Let's iterate validRows and use setValues on specific ranges if we want to be safe, or modify 'allData' and write all.
// Writing ALL overwrites user flags if changed in between.
// Safe approach: Iterate validRows and Update ONLY the calculated columns.
// To optimize, we can group updates if rows are contiguous, but simple loop with setValues is sturdy enough for <500 rows.
// Better: Create a 2D array for just the columns we are touching? No, columns are scattered.
// Let's write row-by-row for now to guarantee correctness of index.
const nameIdx = findC(['Name','FullName', 'name', 'fullname']); // For matching Used Map
validRows.forEach(item => {
const row = item.row;
const rowNum = item.index + 1; // 1-based row number
const adminDaily = (adminDailyIdx > -1) ? row[adminDailyIdx] : '';
const dQ = (adminDaily !== '' && adminDaily !== null && adminDaily !== undefined) ? Number(adminDaily) : adjustedDailyQuota;
const adminLF = (adminLFIdx > -1) ? row[adminLFIdx] : '';
const lfQ = (adminLF !== '' && adminLF !== null && adminLF !== undefined) ? Number(adminLF) : adjustedLFQuota;
// Usage
// We match by Name (from AvailableCapacity row) against the Grid Map keys
const nameVal = (nameIdx > -1) ? String(row[nameIdx]).trim().toLowerCase() : '';
const emailVal = String(row[emailIdx]).trim().toLowerCase();
// Try matching by Name first (as Grid has Names).
// If Grid has IDs/Emails, switch to that.
// seats.gs writes 'userName'. so we use nameVal.
const dUsed = dailyUsedMap[nameVal] || dailyUsedMap[emailVal] || 0;
const lUsed = lfUsedMap[nameVal] || lfUsedMap[emailVal] || 0;
// Update Sheet
// We update: DailyQuota, DailyUsed, DailyLeft, LFQuota, LFUsed, LFLeft
// We can do this in one setValues call if columns are adjacent? Likely not.
// Individual setValue is slow.
// OPTIMIZATION: Write to 'allData' array, then dump the entire 'allData' back to sheet?
// Risk: Overwriting other columns if user edited them while script ran.
// Compromise: Update 'allData' and write back. It's standard for recalculations.
if (dailyQuotaIdx > -1) allData[item.index][dailyQuotaIdx] = dQ;
if (dailyUsedIdx > -1) allData[item.index][dailyUsedIdx] = dUsed;
if (dailyLeftIdx > -1) allData[item.index][dailyLeftIdx] = dQ - dUsed;
if (lfQuotaIdx > -1) allData[item.index][lfQuotaIdx] = lfQ;
if (lfUsedIdx > -1) allData[item.index][lfUsedIdx] = lUsed;
if (lfLeftIdx > -1) allData[item.index][lfLeftIdx] = lfQ - lUsed;
});
// Write back the whole block from stats start?
// Let's write back the whole sheet data to ensure consistency.
sheet.getRange(1, 1, allData.length, allData[0].length).setValues(allData);
Logger.log("Recalculate Complete. Processed " + validRows.length + " rows.");
return { daily: adjustedDailyQuota, lf: adjustedLFQuota };
}
function forceRecalculate() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const sheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const allData = sheet.getDataRange().getValues();
const hIdx = findHeaderRow(allData);
const headers = allData[hIdx];
const quotas = recalculateQuotas(sheet, headers);
return { success: true, quotas: quotas };
}
/* ADDED BY ANTIGRAVITY: Phase Activation and Dynamic Options Logic */
function activatePhase(phaseNum) {
if (!phaseNum) throw new Error("No phase number provided");
PropertiesService.getScriptProperties().setProperty('ACTIVE_PHASE', String(phaseNum));
return { success: true, activePhase: phaseNum };
}
function deactivateAllPhases() {
PropertiesService.getScriptProperties().deleteProperty('ACTIVE_PHASE');
return { success: true, message: 'All phases deactivated. Seat selection closed for all users.' };
}
function getPhaseOptions() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const jagahSheet = ss.getSheetByName(TABS.DAILY_JAGAH);
if (!jagahSheet) return [];
const allData = jagahSheet.getDataRange().getDisplayValues();
const phases = [];
// Phase mapping (Strict row ranges)
const CONFIG = [
{ id: 1, start: 1, end: 8 }, // A2 to A9 (indices 1-8)
{ id: 2, start: 9, end: 16 }, // A10 to A17
{ id: 3, start: 17, end: 24 } // A18 to A25
];
CONFIG.forEach(cfg => {
if (allData[cfg.start] && allData[cfg.start][0]) {
const startLabel = String(allData[cfg.start][0]).trim();
const endLabel = String(allData[cfg.end] ? allData[cfg.end][0] : allData[allData.length-1][0]).trim();
phases.push({
id: cfg.id,
name: "Phase " + cfg.id,
range: startLabel + " - " + endLabel
});
}
});
return phases;
}
/* END OF ADDITION */
/* ADDED BY ANTIGRAVITY: Phase Schedule Storage */
function savePhaseSchedule(schedule) {
if (!schedule) throw new Error("No schedule data provided");
PropertiesService.getScriptProperties().setProperty('PHASE_SCHEDULE', JSON.stringify(schedule));
return { success: true, message: 'Schedule updated successfully' };
}
function getPhaseSchedule() {
const json = PropertiesService.getScriptProperties().getProperty('PHASE_SCHEDULE');
try {
return json ? JSON.parse(json) : { p1: '', p2: '', p3: '' };
} catch (e) {
return { p1: '', p2: '', p3: '' };
}
}
/* END OF ADDITION */
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<?!= include('styles'); ?>
</head>
<body class="admin-layout">
<!-- Modal System (Styles in styles.html) -->
<div id="modal-overlay" class="modal-overlay">
<div id="modal-box" class="modal-box">
<div id="modal-title" class="modal-title">Notice</div>
<div id="modal-message" class="modal-message"></div>
<button class="modal-btn" onclick="closeModal()">OK</button>
</div>
</div>
<script>
let modalCallback = null;
function showModal(title, message, type = 'info', callback = null) {
const overlay = document.getElementById('modal-overlay');
const box = document.getElementById('modal-box');
const titleEl = document.getElementById('modal-title');
const messageEl = document.getElementById('modal-message');
modalCallback = callback; // Store callback
box.className = 'modal-box';
if (type === 'error') box.classList.add('modal-error');
if (type === 'success') box.classList.add('modal-success');
titleEl.innerText = title;
messageEl.innerHTML = message;
overlay.classList.add('flex');
overlay.style.display = 'flex';
}
function closeModal() {
document.getElementById('modal-overlay').style.display = 'none';
document.getElementById('modal-overlay').classList.remove('flex');
if (modalCallback) {
modalCallback();
modalCallback = null;
}
}
function doAdminLogout() {
// Clear any saved data
localStorage.removeItem('shehrullah_email');
sessionStorage.removeItem('shehrullah_pref_view');
// Redirect to user dashboard page (index.html) with logout parameter
// Use window.open with _top target to ensure we fully break out of Admin context
google.script.run.withSuccessHandler(function (url) {
const targetUrl = url + '?logout=true';
window.open(targetUrl, '_top');
}).getScriptUrl();
}
</script>
<div class="nav-panel">
<div class="brand" style="text-align: center; padding: 15px 10px;">
<img src="https://i.imgur.com/UbBWvAo.png" alt="Shehrullah Header"
style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
</div>
<div class="nav-item active" onclick="showSection('dashboard', this)">
📊
Dashboard
</div>
<div class="nav-item" onclick="showSection('behnos', this)">
<img src="https://i.imgur.com/DMNKoCB.png" alt="Behno"
style="width: 20px; height: 20px; object-fit: contain;">
Behno
</div>
<div class="nav-item" onclick="showSection('daily-jagah', this)">
📅
Daily Jagah
</div>
<div id="nav-lf-jagah" class="nav-item" onclick="showSection('lf-jagah', this)">
🌙
LF Jagah
</div>
<div style="flex:1"></div>
<div class="nav-item" onclick="goToUserDashboard()">
👤
User Dashboard
</div>
<div class="nav-item" onclick="doAdminLogout()">
🚪
Logout
</div>
</div>
<div class="content">
<!-- Dashboard Section -->
<div id="dashboard" class="section active">
<!-- ADDED BY ANTIGRAVITY: Redundant header removed as requested -->
<!-- <h1>Dashboard Overview</h1> -->
<!-- END OF REDUCTION -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Total Behno</div>
<div class="stat-value" id="stat-total">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Daily Quota</div>
<div class="stat-value" id="stat-daily">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Layali Fadela Quota</div>
<div class="stat-value" id="stat-lf">-</div>
</div>
</div>
<h2>Phase Schedule</h2>
<div
style="background:#fff; padding:20px; border-radius:12px; box-shadow:0 2px 4px rgba(0,0,0,0.05); margin-bottom: 30px;">
<p style="margin-bottom:15px; color:#666; font-size:0.95rem;">Set the dates visible to users. This does
NOT automatically activate phases (use buttons below).</p>
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:15px; margin-bottom:15px;">
<div>
<label style="font-size:0.85rem; font-weight:600; color:#555;">Phase 1 Start</label>
<input type="datetime-local" id="sched-p1-input" class="search-bar">
</div>
<div>
<label style="font-size:0.85rem; font-weight:600; color:#555;">Phase 2 Start</label>
<input type="datetime-local" id="sched-p2-input" class="search-bar">
</div>
<div>
<label style="font-size:0.85rem; font-weight:600; color:#555;">Phase 3 Start</label>
<input type="datetime-local" id="sched-p3-input" class="search-bar">
</div>
</div>
<button class="btn" style="padding:8px 20px; font-size:0.9rem;" onclick="saveSchedule()">Save Schedule
Dates</button>
</div>
<h2>Phase Management</h2>
<div style="background:#fff; padding:20px; border-radius:12px; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
<p style="margin-bottom:15px; color:#666; font-size:0.95rem;">Select a phase to activate seat bookings
for all eligible members.</p>
<select id="phase-select" style="width:100%; max-width:500px; margin-bottom:15px; display:block;"
onchange="checkPhaseStatus()">
<option value="">Select Phase to Activate</option>
<!-- ADDED BY ANTIGRAVITY: Commented out hard-coded dates as requested -->
<!--
<option value="1">Phase 1: Mon, 02/16/2026 - Mon, 02/23/2026</option>
<option value="2">Phase 2: Tue, 02/24/2026 - Tue, 03/03/2026</option>
<option value="3">Phase 3: Thu, 03/05/2026 - Mon, 03/16/2026</option>
-->
<!-- END OF ADDITION -->
</select>
<div id="phase-action-container" style="margin-top: 15px;">
<!-- Dynamic button will appear here based on selection -->
</div>
<div id="current-phase-status"
style="margin-top: 15px; padding: 10px; background: #f0f8ff; border-left: 4px solid var(--color-accent); border-radius: 4px; font-size: 0.9rem;">
<strong>Currently Active Phase:</strong> <span id="active-phase-display"
style="color: var(--color-accent); font-weight: 600;">None</span>
</div>
</div>
</div>
<!-- Shukran Section -->
<div id="shukran" class="section">
<div style="display: flex; justify-content: center; align-items: center; min-height: calc(100vh - 50px);">
<div
style="background: #fff; padding: 40px 60px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); text-align: center; width: 100%; max-width: 450px;">
<h1 style="color: var(--color-accent); font-size: 3.5rem; margin:0;">Shukran</h1>
</div>
</div>
</div>
<!-- Behnos Section -->
<div id="behnos" class="section">
<h1>Behno</h1>
<div class="table-container">
<div class="table-controls">
<input type="text" id="search" class="search-bar" placeholder="Search by name or email..."
onkeyup="filterTable()" style="max-width: 300px;">
<!-- ADDED BY ANTIGRAVITY: Recalculate Quotas button removed as requested -->
<!--
<button class="btn" style="padding:6px 12px; font-size:0.85rem;"
onclick="doRecalculate()">Recalculate Quotas</button>
-->
<!-- END OF REDUCTION -->
<label style="cursor:pointer; white-space: nowrap;">
<input type="checkbox" id="preflock-all"> Lock All Preferences
</label>
</div>
<table id="behno-table">
<thead>
<tr>
<th>Name</th>
<th>Attnd</th>
<th>QuotaDaily</th>
<th>AdminDaily</th>
<th>UsedDaily</th>
<th>LeftDaily</th>
<th>QuotaLF</th>
<th>AdminLF</th>
<th>UsedLF</th>
<th>LeftLF</th>
<th>Lock</th>
<th>Notes</th>
</tr>
</thead>
<tbody id="behno-body"></tbody>
</table>
<div class="pagination" id="pagination"></div>
</div>
</div>
<!-- Daily Jagah Section -->
<div id="daily-jagah" class="section">
<h1>Daily Jagah</h1>
<div class="table-container">
<div id="daily-jagah-content"></div>
<div class="pagination" id="daily-jagah-pagination"></div>
</div>
</div>
<!-- LF Jagah Section -->
<div id="lf-jagah" class="section">
<h1>Layali Fadela Jagah</h1>
<div class="table-container" id="lf-content-wrapper">
<div id="lf-jagah-content"></div>
<div class="pagination" id="lf-jagah-pagination"></div>
</div>
</div>
</div>
<script>
let adminData = null;
let behnoData = [];
let filteredData = [];
let behnoPage = 1;
let dailyJagahPage = 1;
let lfJagahPage = 1;
const rowsPerPage = 10;
const jagahRowsPerPage = 10;
window.onload = function () {
// Show loading state
document.getElementById('stat-total').innerText = 'Loading...';
document.getElementById('stat-daily').innerText = 'Loading...';
document.getElementById('stat-lf').innerText = 'Loading...';
loadData();
/* ADDED BY ANTIGRAVITY: Load Phase Options on init */
loadPhaseOptions();
/* END OF ADDITION */
};
function loadData() {
google.script.run
.withSuccessHandler(render)
.withFailureHandler(function (error) {
console.error('Failed to load admin data:', error);
showModal('Error Loading Dashboard',
'Failed to load admin dashboard data. ' + error.message +
'<br><br>Click OK to return to login page.',
'error',
function () {
google.script.run.withSuccessHandler(function (url) {
window.open(url, '_top');
}).getScriptUrl();
});
})
.getAdminData();
}
function render(data) {
adminData = data;
/* ADDED BY ANTIGRAVITY: Storing active phase for button state */
window.activePhaseId = data.activePhase || '';
/* END OF ADDITION */
/* ADDED BY ANTIGRAVITY: Update Display Text */
const activeDisplay = document.getElementById('active-phase-display');
if (activeDisplay) {
activeDisplay.innerText = data.activePhase ? ("Phase " + data.activePhase) : "None";
}
// Auto-set Admin Preference since we are on Admin Dashboard
sessionStorage.setItem('shehrullah_pref_view', 'admin');
/* END OF ADDITION */
document.getElementById('stat-total').innerText = data.stats.total;
document.getElementById('stat-daily').innerText = data.stats.dailyQuota;
document.getElementById('stat-lf').innerText = data.stats.lfQuota;
document.getElementById('stat-lf').innerText = data.stats.lfQuota;
// CONDITIONAL VISIBILITY FOR LF JAGAH
const activePhase = parseInt(data.activePhase) || 0;
const lfSection = document.getElementById('lf-jagah');
const lfNav = document.getElementById('nav-lf-jagah');
if (activePhase < 2) {
// ADDED BY ANTIGRAVITY: Removed hiding logic for Admin view as requested
/*
// COMPLETELY HIDE UI ELEMENTS
if (lfNav) lfNav.style.display = 'none';
if (lfSection) lfSection.style.display = 'none';
// If currently viewing LF Jagah, switch back to dashboard
if (lfSection && lfSection.classList.contains('active')) {
showSection('dashboard', document.querySelector('.nav-item.active'));
}
*/
} else {
if (lfNav) lfNav.style.display = 'flex'; // Restore nav item
// Section display is handled by showSection class toggling, so don't force block here
if (lfSection) lfSection.style.display = '';
document.getElementById('lf-content-wrapper').style.display = 'block';
}
behnoData = data.behnos;
filteredData = behnoData;
renderBehnoTable();
renderJagahGrid('Daily');
// Always render LF grid for Admin
renderJagahGrid('LF');
// Populate Schedule Inputs
if (data.phaseSchedule) {
document.getElementById('sched-p1-input').value = data.phaseSchedule.p1 || '';
document.getElementById('sched-p2-input').value = data.phaseSchedule.p2 || '';
document.getElementById('sched-p3-input').value = data.phaseSchedule.p3 || '';
}
}
function renderBehnoTable() {
const tbody = document.getElementById('behno-body');
tbody.innerHTML = '';
let rowsPerPage = 20; // UPDATED: Show 20 names per page
const start = (behnoPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
const pageData = filteredData.slice(start, end);
// Helper to render 0 correctly instead of blank
const safeVal = (v) => (v !== undefined && v !== null && v !== '') ? v : '';
// Helper to render numeric 0 correctly (allow 0, but empty string becomes empty)
const numVal = (v) => (v !== undefined && v !== null && v !== '') ? v : '';
pageData.forEach(row => {
const tr = document.createElement('tr');
const name = row.Name || row.name || '';
const email = row.Email || row.email || '';
// Get raw values trying both cases
const dq = row.DailyQuota ?? row.dailyquota;
const da = row.DailyAdmin ?? row.dailyadmin;
const du = row.DailyUsed ?? row.dailyused;
const dl = row.DailyLeft ?? row.dailyleft;
const lfq = row.LFQuota ?? row.lfquota;
const lfa = row.LFAdmin ?? row.lfadmin;
const lfu = row.LFUsed ?? row.lfused;
const lfl = row.LFLeft ?? row.lfleft;
tr.innerHTML = `
<td data-label="Name">${name}</td>
<td data-label="Attnd">${row.Attendance || row.attendance || ''}</td>
<td data-label="QuotaDaily">${numVal(dq)}</td>
<td data-label="AdminDaily"><input class="edit-cell" type="number" value="${numVal(da)}" onchange="updateVal('${email}', 'DailyAdmin', this.value)"></td>
<td data-label="UsedDaily">${numVal(du)}</td>
<td data-label="LeftDaily">${numVal(dl)}</td>
<td data-label="QuotaLF">${numVal(lfq)}</td>
<td data-label="AdminLF"><input class="edit-cell" type="number" value="${numVal(lfa)}" onchange="updateVal('${email}', 'LFAdmin', this.value)"></td>
<td data-label="UsedLF">${numVal(lfu)}</td>
<td data-label="LeftLF">${numVal(lfl)}</td>
<td data-label="Lock"><input type="checkbox" ${row.PrefLock || row.preflock ? 'checked' : ''} onchange="updateVal('${email}', 'PrefLock', this.checked)"></td>
<td data-label="Notes"><input class="edit-cell" type="text" value="${row.Notes || row.notes || ''}" onchange="updateVal('${email}', 'Notes', this.value)" style="width:100px;"></td>
`;
tbody.appendChild(tr);
});
renderPagination('pagination', filteredData.length, rowsPerPage, behnoPage, 'behnoPage');
}
function renderJagahGrid(type) {
const data = (type === 'Daily') ? adminData.dailyJagah : adminData.lfJagah;
const container = (type === 'Daily') ? document.getElementById('daily-jagah-content') : document.getElementById('lf-jagah-content');
if (!data || !data.dates || data.dates.length === 0) {
container.innerHTML = '<p>No data found.</p>';
return;
}
// LF shows all dates (no pagination)
const isLF = (type === 'LF');
const page = isLF ? 1 : dailyJagahPage;
const perPage = isLF ? 999 : jagahRowsPerPage;
const start = (page - 1) * perPage;
const end = start + perPage;
const pageDates = data.dates.slice(start, end);
let html = `<table><thead><tr><th>Date</th>`;
data.seats.forEach(s => html += `<th>${s}</th>`);
html += `</tr></thead><tbody>`;
pageDates.forEach(date => {
let rowHtml = '';
let filledCount = 0;
data.seats.forEach(seat => {
const occupant = data.mapping[date + "|" + seat];
const bgColor = occupant ? '' : (type === 'Daily' ? '#727AC0' : '#F7EBF2');
if (occupant) filledCount++;
// UPDATED: Color #3e7876 and Bold for Occupied Seats
const cellStyle = occupant ? 'color:#3e7876; font-weight:700;' : 'color:#333;';
rowHtml += `<td data-label="${seat}" style="background-color:${bgColor}; ${cellStyle} font-size:0.75rem;">${occupant || '-'}</td>`;
});
const isFullyBooked = (filledCount === data.seats.length);
const rowStyle = isFullyBooked ? 'style="background-color:#9CF4DF;"' : '';
html += `<tr ${rowStyle}><td data-label="Date">${date}</td>${rowHtml}</tr>`;
});
html += `</tbody></table>`;
container.innerHTML = html;
if (!isLF) {
renderPagination('daily-jagah-pagination', data.dates.length, jagahRowsPerPage, page, 'dailyJagahPage');
} else {
document.getElementById('lf-jagah-pagination').innerHTML = '';
}
}
function renderPagination(containerId, totalRows, perPage, current, pageVarName) {
const pagination = document.getElementById(containerId);
const totalPages = Math.ceil(totalRows / perPage);
let html = '';
for (let i = 1; i <= totalPages; i++) {
html += `<button class="page-btn ${i === current ? 'active' : ''}" onclick="${pageVarName} = ${i}; ${(pageVarName === 'behnoPage' ? 'renderBehnoTable()' : 'renderJagahGrid(\'' + (pageVarName.includes('daily') ? 'Daily' : 'LF') + '\')')}">${i}</button>`;
}
pagination.innerHTML = html;
}
function goToPage(page) {
behnoPage = page;
renderBehnoTable();
}
function updateVal(email, field, val) {
// Find by exact email match (case insensitive)
const row = behnoData.find(r => (r.Email || r.email || '').toLowerCase() === email.toLowerCase());
if (row) row[field] = val;
google.script.run
.withSuccessHandler(res => {
loadData(); // Still reload to ensure server-side quotas are synced
})
.updateAdminValue(email, field, val);
renderBehnoTable(); // Reactive update
}
function filterTable() {
const q = document.getElementById('search').value.toLowerCase();
filteredData = behnoData.filter(row => {
const name = (row.Name || row.name || '').toLowerCase();
const email = (row.Email || row.email || '').toLowerCase();
return name.includes(q) || email.includes(q);
});
behnoPage = 1;
renderBehnoTable();
}
function showSection(id, el) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById(id).classList.add('active');
if (el) el.classList.add('active');
}
function goToUserDashboard() {
// Get the user's email to pass in URL for auto-login
// FIX: Use currentUserEmail (backend property) instead of userEmail (incorrect)
const userEmail = (adminData && adminData.currentUserEmail) || localStorage.getItem('shehrullah_email') || '';
console.log('goToUserDashboard called');
console.log('adminData:', adminData);
console.log('localStorage email:', localStorage.getItem('shehrullah_email'));
console.log('userEmail to pass:', userEmail);
if (adminData && adminData.scriptUrl) {
// Ensure we append forceUser=true and email correctly
const baseUrl = adminData.scriptUrl.split('?')[0];
const targetUrl = baseUrl + '?forceUser=true&email=' + encodeURIComponent(userEmail);
window.open(targetUrl, '_top');
} else {
google.script.run.withSuccessHandler(url => {
const baseUrl = url.split('?')[0];
const targetUrl = baseUrl + '?forceUser=true&email=' + encodeURIComponent(userEmail);
window.open(targetUrl, '_top');
}).getScriptUrl();
}
}
function doLogout() {
localStorage.removeItem('shehrullah_email');
showSection('shukran');
// Remove navigation to prevent going back
document.querySelector('.nav-panel').style.display = 'none';
}
function activatePhase() {
const phase = document.getElementById('phase-select').value;
if (!phase) {
showModal("Selection Required", "Please select a phase to activate.", "info");
return;
}
/* ADDED BY ANTIGRAVITY: Linked phase activation to backend */
const btn = document.querySelector('button[onclick="activatePhase()"]');
const originalText = btn.innerText;
btn.innerText = "Activating...";
btn.disabled = true;
google.script.run
.withSuccessHandler(res => {
btn.innerText = originalText;
btn.disabled = false;
/* ADDED BY ANTIGRAVITY: Update local state and button text on success */
window.activePhaseId = phase;
checkPhaseStatus();
// Update Text Display
const activeDisplay = document.getElementById('active-phase-display');
if (activeDisplay) {
activeDisplay.innerText = "Phase " + phase;
}
/* END OF ADDITION */
showModal("Success", "Phase " + phase + " is now active for all users!", "success");
})
.withFailureHandler(e => {
btn.innerText = originalText;
btn.disabled = false;
showModal("Error", e.message, "error");
})
.activatePhase(phase);
/* END OF ADDITION */
}
/* ADDED BY ANTIGRAVITY: Logic to update button text based on status */
function checkPhaseStatus() {
const select = document.getElementById('phase-select');
const btn = document.querySelector('button[onclick="activatePhase()"]');
const val = select.value;
if (val && val === window.activePhaseId) {
btn.innerText = 'Activated';
btn.classList.add('btn-success'); // Assuming a styling preference or just changing text
btn.style.background = 'var(--color-accent)';
} else {
btn.innerText = 'Activate Selected Phase';
btn.style.background = 'var(--color-primary)';
}
}
/* END OF ADDITION */
/* ADDED BY ANTIGRAVITY: Logic to populate Phase Dropdown */
function loadPhaseOptions() {
const select = document.getElementById('phase-select');
google.script.run
.withSuccessHandler(phases => {
// Keep the default option
const defaultOpt = select.options[0];
select.innerHTML = '';
select.appendChild(defaultOpt);
phases.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.innerText = p.name + ": " + p.range;
select.appendChild(opt);
});
})
.getPhaseOptions();
}
/* END OF ADDITION */
function doRecalculate() {
const btn = document.querySelector('button[onclick="doRecalculate()"]');
const originalText = btn.innerText;
btn.innerText = "Processing...";
btn.disabled = true;
google.script.run
.withSuccessHandler(() => {
showModal("Success", "Quotas recalculated successfully.", "success");
btn.innerText = originalText;
btn.disabled = false;
// Refresh data
google.script.run.withSuccessHandler(render).getAdminData();
})
.withFailureHandler(e => {
showModal("Error", e.message, "error");
btn.innerText = originalText;
btn.disabled = false;
})
.forceRecalculate();
}
function goToUserDashboard() {
// Simplified Logic: Go to Main URL -> Triggers Admin Choice Page (per user request)
const scriptUrl = window.adminData ? window.adminData.scriptUrl : '';
const email = (window.adminData && window.adminData.currentUserEmail) ? window.adminData.currentUserEmail : '';
// Clear any user preference so Index logic treats this as a fresh Admin login -> Shows Choice
sessionStorage.removeItem('shehrullah_pref_view');
// Construct safe URL
let targetUrl = scriptUrl ? scriptUrl.split('?')[0] : null;
// Only append email if it exists and is valid
if (targetUrl && email) {
targetUrl += '?email=' + encodeURIComponent(email);
}
if (targetUrl) {
// Open clean URL in top window
window.open(targetUrl, '_top');
} else {
// Fallback if scriptUrl wasn't in adminData (e.g. data load failed)
google.script.run.withSuccessHandler(function (url) {
const baseUrl = url.split('?')[0];
// Here we might miss the email if adminData failed, but at least we go to the page
window.open(baseUrl, '_top');
}).getScriptUrl();
}
}
/*
function doAdminLogout() {
sessionStorage.removeItem('shehrullah_pref_view');
showSection('shukran-admin', null);
}
*/
// Schedule Functions
function saveSchedule() {
const schedule = {
p1: document.getElementById('sched-p1-input').value,
p2: document.getElementById('sched-p2-input').value,
p3: document.getElementById('sched-p3-input').value
};
const btn = document.querySelector('button[onclick="saveSchedule()"]');
const originalText = btn.innerText;
btn.innerText = "Saving...";
btn.disabled = true;
google.script.run
.withSuccessHandler(res => {
showModal("Success", "Schedule dates updated.", "success");
btn.innerText = originalText;
btn.disabled = false;
})
.withFailureHandler(e => {
showModal("Error", e.message, "error");
btn.innerText = originalText;
btn.disabled = false;
})
.savePhaseSchedule(schedule);
}
// Phase Management Functions
function checkPhaseStatus() {
const select = document.getElementById('phase-select');
const selectedPhase = select.value;
const activePhase = window.activePhaseId || '';
const container = document.getElementById('phase-action-container');
const statusDisplay = document.getElementById('active-phase-display');
// Update active phase display
if (activePhase) {
const activeOption = Array.from(select.options).find(opt => opt.value === activePhase);
statusDisplay.textContent = activeOption ? activeOption.text : `Phase ${activePhase}`;
} else {
statusDisplay.textContent = 'None';
}
// Show appropriate button based on selection
if (!selectedPhase) {
container.innerHTML = '';
return;
}
if (selectedPhase === activePhase) {
// Selected phase is currently active - show Deactivate button
container.innerHTML = `
<button class="btn" style="background:#E2727C; color:#fff; border-radius:8px; padding:10px 20px;"
onclick="deactivateCurrentPhase()">
Deactivate This Phase
</button>
`;
} else {
// Selected phase is not active - show Activate button
container.innerHTML = `
<button class="btn btn-primary" style="background:var(--color-primary); color:#fff; border-radius:8px; padding:10px 20px;"
onclick="activatePhase()">
Activate This Phase
</button>
`;
}
}
function activatePhase() {
const select = document.getElementById('phase-select');
const phaseNum = select.value;
if (!phaseNum) {
showModal('Error', 'Please select a phase to activate', 'error');
return;
}
google.script.run
.withSuccessHandler(function (result) {
showModal('Success', `Phase ${phaseNum} activated successfully! Users can now select seats.`, 'success');
window.activePhaseId = phaseNum;
checkPhaseStatus(); // Update button display
})
.withFailureHandler(function (error) {
showModal('Error', 'Failed to activate phase: ' + error.message, 'error');
})
.activatePhase(phaseNum);
}
function deactivateCurrentPhase() {
google.script.run
.withSuccessHandler(function (result) {
showModal('Success', result.message || 'Phase deactivated. Seat selection closed for all users.', 'success');
window.activePhaseId = '';
document.getElementById('phase-select').value = '';
checkPhaseStatus(); // Update button display
})
.withFailureHandler(function (error) {
showModal('Error', 'Failed to deactivate phase: ' + error.message, 'error');
})
.deactivateAllPhases();
}
</script>
</body>
</html>
const DATA_SHEET_ID = '1g5KcJXInI3myNSTTT__XXM1mUkrSTxSL1R56KOFFSoU';
//const DATA_SHEET_ID = '1hijdA9iY4jq9PoLsTs2c50L0hbDpIfUy2oXl63Mv6q8'; sfjamaat a/c
//const GRID_SHEET_ID = '1trjTMXlpMFcajq9riRACAmE6Ex5r3QqeK2Zn5OrTXBM';
const VERSION = "V4.9.0 (Refactored Codebase - Extracted CSS & Preferences)";
const TABS = {
DATA: 'data',
DATES: 'Dates',
SEATS: 'Seats',
USER_MASTER: 'UserMaster',
AVAILABLE_CAPACITY: 'AvailableCapacity',
DAILY_JAGAH: 'DailyJagah',
LF_JAGAH: 'LFJagah'
};
function doGet(e) {
const page = e.parameter.page || 'index';
const template = HtmlService.createTemplateFromFile(page === 'AdminDashboard' ? 'AdminDashboard' : 'index');
template.urlParams = e.parameter;
return template.evaluate()
.setTitle(page === 'AdminDashboard' ? 'Shehrullah Admin Dashboard' : 'Shehrullah Dashboard')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function getScriptUrl() {
return ScriptApp.getService().getUrl();
}
/**
* Robustly maps a row to an object using case-insensitive/trimmed headers
*/
function mapRowToObj(rawHeaders, row) {
const obj = {};
rawHeaders.forEach((h, i) => {
if (!h) return;
const key = String(h).trim();
obj[key] = row[i];
obj[key.toLowerCase()] = row[i];
});
return obj;
}
// Normalizer for keys (Lower, no spaces, no special chars)
const kNorm = (v) => String(v || '').toLowerCase().replace(/[^a-z0-9]/g, '').trim();
/**
* Finds the header row index (0-indexed) based on critical keywords
*/
function findHeaderRow(allData) {
for (let i = 0; i < Math.min(allData.length, 10); i++) {
const row = allData[i].map(c => String(c).trim().toLowerCase());
// Require at least 2 critical headers to be sure
const hits = ['email', 'fullname', 'hofid', 'misaq', 'misaaq', 'age', 'seat', 'attendancemode'].filter(k => row.some(cell => cell.includes(k)));
if (hits.length >= 2) return i;
}
// Second pass: if only one matches but it's very distinct
for (let i = 0; i < Math.min(allData.length, 10); i++) {
const row = allData[i].map(c => String(c).trim().toLowerCase());
if (row.includes('email') && row.includes('hofid')) return i;
}
return 0; // Fallback
}
/**
* Maps all data rows to objects using robust header detection
*/
function fetchTabRows(ss, tabName) {
const sheet = ss.getSheetByName(tabName);
if (!sheet) return { rows: [], headers: [], hIdx: -1 };
const allData = sheet.getDataRange().getValues();
if (allData.length === 0) return { rows: [], headers: [], hIdx: -1 };
const hIdx = findHeaderRow(allData);
const rawHeaders = allData[hIdx];
const rows = allData.slice(hIdx + 1).map((r, i) => {
const obj = mapRowToObj(rawHeaders, r);
obj._rowNum = hIdx + i + 2; // 1-indexed spreadsheet row
return obj;
});
return { rows, headers: rawHeaders, hIdx };
}
/* UPDATE by Antigravity: Strictly enforce eligibility and data flow */
function loginUser(email) {
if (!email) throw new Error("Please enter an email.");
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const sheet = ss.getSheetByName(TABS.DATA);
if (!sheet) throw new Error("System Error: 'data' tab not found.");
const allData = sheet.getDataRange().getValues();
if (allData.length < 2) throw new Error("System Error: 'data' tab is empty.");
const { rows, headers: rawHeaders, hIdx } = fetchTabRows(ss, TABS.DATA);
const headers = rawHeaders.map(h => String(h).trim().toLowerCase());
// Locate Columns
const findCol = (targets) => {
for(let t of targets) {
const idx = headers.indexOf(t.toLowerCase());
if (idx !== -1) return idx;
}
return headers.findIndex(h => targets.some(t => h.includes(t.toLowerCase())));
};
const emailIdx = findCol(['Email']);
const hofIdIdx = findCol(['HOFIDs', 'HOFID']);
const ageIdx = findCol(['Age']);
// Eligibility Columns
let misaqIdx = headers.indexOf('misaq');
if (misaqIdx === -1) misaqIdx = headers.indexOf('misaaq'); // Explicit check
if (misaqIdx === -1) misaqIdx = headers.findIndex(h => h.includes('misaq')); // Fuzzy
const dkrIdx = findCol(['Dkr', 'Status']);
const adminIdx = headers.indexOf('admin');
const nameIdx = findCol(['Name', 'FullName']);
const primaryIdx = findCol(['Primary', 'IsPrimary']);
const searchEmail = String(email).trim().toLowerCase();
// 1. Locate User Row(s)
const recordsWithEmail = rows.filter(r => String(r[rawHeaders[emailIdx]]).trim().toLowerCase() === searchEmail);
if (recordsWithEmail.length === 0) throw new Error("Email not found in database. Contact Admin.");
// Choose canonical user (highest age or first)
const userObj = recordsWithEmail.sort((a,b) => (b.Age || b.age || 0) - (a.Age || a.age || 0))[0];
const userUid = userObj._rowNum;
const rawUserRow = allData[userUid - 1];
const userData = mapRowToObj(rawHeaders, rawUserRow);
// 3. Find HOFID from ANY record with this email
let myHofId = '';
for (let r of recordsWithEmail) {
const h = String(r[rawHeaders[hofIdIdx]] || '').trim();
if (h && h !== '-' && h !== '0') { myHofId = h; break; }
}
// Helper: Eligibility Value Check (Case-insensitive)
const isOk = (val) => ['done', 'yes', 'y', 'ok', 'true'].includes(String(val || '').trim().toLowerCase());
// 4. Strict Eligibility Check (Primary Login)
if (misaqIdx > -1) {
const val = String(rawUserRow[misaqIdx] || '').trim().toLowerCase();
if (!isOk(val)) throw new Error("Misaaq status not valid (" + (val||'Empty') + "). Contact Admin.");
}
if (dkrIdx > -1) {
const val = String(rawUserRow[dkrIdx] || '').trim().toUpperCase();
if (val === 'B') throw new Error("Status is restricted (B). Please contact Admin.");
}
// Group Family by HOFID
const familyGroup = rows.filter(r => String(r[rawHeaders[hofIdIdx]]).trim() === myHofId).map(f => {
const r = allData[f._rowNum - 1];
return {
...f,
raw: r,
age: parseFloat(String(r[ageIdx] || '0').replace(/[^\d.]/g, '')) || 0,
isPrimaryFlag: (primaryIdx > -1 && ['yes', 'y'].includes(String(r[primaryIdx]).trim().toLowerCase()))
};
});
// ... (Primary Logic preserved) ...
// 5. Determine Primary Status
const primaryColIdx = findCol(['Primary', 'IsPrimary']);
let primaryUser = familyGroup.find(f => f.isPrimaryFlag);
if (!primaryUser) {
const sorted = [...familyGroup].sort((a,b) => b.age - a.age);
if (sorted.length > 0) primaryUser = sorted[0];
}
let activeUser = userData;
let activeUid = userUid;
let isPrimary = false;
if (primaryUser) {
const primaryEmail = String(primaryUser.raw[emailIdx] || '').trim().toLowerCase();
if (primaryEmail === searchEmail) {
isPrimary = true;
activeUser = mapRowToObj(rawHeaders, primaryUser.raw);
activeUid = primaryUser._rowNum;
}
}
activeUser.uid = activeUid;
const isAdminUser = adminIdx > -1 ? ['yes', 'y'].includes(String(activeUser[String(rawHeaders[adminIdx]).trim()]||'').toLowerCase()) : false;
const allPrefsMap = getUserPreferencesMap();
const myNormName = kNorm(activeUser.FullName || activeUser.Name || '');
// 7. Map family data
const familyMembers = familyGroup.map(f => {
const mObj = mapRowToObj(rawHeaders, f.raw);
const memEmail = (mObj.Email || mObj.email || searchEmail).toLowerCase();
const prefKey = memEmail + "|" + kNorm(mObj.Name || mObj.FullName || '');
// STRICT Eligibility Check for Family
// Relaxed slightly to accept 'Yes'/'Y' etc.
const fMisaq = misaqIdx > -1 ? String(f.raw[misaqIdx] || '').trim().toLowerCase() : '';
const fDkr = dkrIdx > -1 ? String(f.raw[dkrIdx] || '').trim().toUpperCase() : '';
// NEW LOGIC: 'Done' or 'Yes' is valid. Blank is INVALID.
const isMisaqDone = isOk(fMisaq);
const isStatusOk = (fDkr !== 'B');
const isEligible = (isMisaqDone && isStatusOk);
const seatStat = getFamilySeatStatus(mObj.Email);
const nameCandidates = [
mObj.Name, mObj.name,
mObj.FullName, mObj.fullname,
mObj['Full Name'], mObj['full name'],
f.Name, f.FullName
].filter(v => v);
let foundPrefs = null;
// 1. Try Email + Name combo
for (let n of nameCandidates) {
const key = memEmail + "|" + kNorm(n);
if (allPrefsMap[key]) { foundPrefs = allPrefsMap[key]; break; }
}
// 2. Try Name only fallback
if (!foundPrefs) {
for (let n of nameCandidates) {
const key = kNorm(n);
if (allPrefsMap[key]) { foundPrefs = allPrefsMap[key]; break; }
}
}
return {
uid: f._rowNum,
Name: mObj.Name || mObj.FullName || 'User',
FullName: mObj.FullName || mObj.Name || 'User',
Email: mObj.Email || '',
'Cell#': mObj['Cell#'] || mObj.Phone || "",
preferences: foundPrefs,
isEligible: isEligible,
seatStatus: { hasSeat: (seatStat.bookingCount > 0) }
};
});
const loginNormName = kNorm(activeUser.FullName || activeUser.Name || '');
const userCandidates = [
activeUser.FullName, activeUser.fullname,
activeUser.Name, activeUser.name,
activeUser['Full Name'], activeUser['full name']
].filter(v => v);
let userFoundPrefs = null;
for (let n of userCandidates) {
const key = searchEmail + "|" + kNorm(n);
if (allPrefsMap[key]) { userFoundPrefs = allPrefsMap[key]; break; }
}
if (!userFoundPrefs) {
for (let n of userCandidates) {
const key = kNorm(n);
if (allPrefsMap[key]) { userFoundPrefs = allPrefsMap[key]; break; }
}
}
return {
success: true,
user: activeUser, // Return the Resolved Primary Identity
isAdmin: isAdminUser,
isPrimary: isPrimary,
familyMembers: familyMembers,
userQuotas: getUserQuotas(searchEmail),
userPreferences: userFoundPrefs,
myBookings: getUserBookings(searchEmail),
seatLabels: getGlobalSeatLabels(),
activePhase: PropertiesService.getScriptProperties().getProperty('ACTIVE_PHASE') || '',
phaseSchedule: getPhaseSchedule(), // Return Schedule
scriptUrl: getScriptUrl(), // Return URL for instant navigation
version: VERSION
};
}
function getAdminContacts() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const { rows, headers: rawHeaders } = fetchTabRows(ss, TABS.DATA);
const headers = rawHeaders.map(h => String(h).trim().toLowerCase());
const adminIdx = headers.indexOf('admin');
const nameIdx = headers.indexOf('name');
const phoneIdx = headers.indexOf('cell#');
if (adminIdx === -1) return "System Error: Admin column not found.";
const admins = rows.filter(r => {
const val = String(r[rawHeaders[adminIdx]] || '').trim().toLowerCase();
return ['yes', 'y', 'true'].includes(val);
}).map(r => {
const name = String(r[rawHeaders[nameIdx]] || r.FullName || r.fullname || 'Admin').trim();
const phone = String(r[rawHeaders[phoneIdx]] || '').trim();
return `• <b>${name}:</b> ${phone}`;
});
return admins.join('<br>');
}
function getUserPreferencesMap() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const sheet = ss.getSheetByName(TABS.USER_MASTER);
if (!sheet) return {};
const data = sheet.getDataRange().getValues();
if (data.length < 2) return {};
const rawHeaders = data[0];
const headers = rawHeaders.map(h => String(h).trim().toLowerCase());
const emailIdx = headers.indexOf('email');
const findN = (ts) => headers.findIndex(h => ts.some(t => h.includes(t.toLowerCase())));
const nameIdx = findN(['fullname', 'name', 'user']);
if (emailIdx === -1 || nameIdx === -1) return {};
const map = {};
data.slice(1).forEach(row => {
const email = String(row[emailIdx] || '').trim().toLowerCase();
const name = kNorm(row[nameIdx] || '');
const obj = mapRowToObj(rawHeaders, row);
if (email) {
map[email + "|" + name] = obj;
}
// Fallback: also store by name only if not already present or if email is blank
if (name && !map[name]) {
map[name] = obj;
}
});
return map;
}
function submitPreferences(form) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const umSheet = ss.getSheetByName(TABS.USER_MASTER);
const umData = umSheet.getDataRange().getValues();
const rawHeaders = umData[0];
const headers = rawHeaders.map(h => String(h).trim().toLowerCase());
const emailIdx = headers.indexOf('email');
const nameIdx = headers.findIndex(h => h === 'name' || h === 'fullname');
const dataTabUser = getUserFromDataTab(form.email, form.uid);
const searchEmail = String(form.email).trim().toLowerCase();
const searchName = kNorm(dataTabUser.FullName || dataTabUser.fullname || dataTabUser.Name || dataTabUser.name || '');
// Find row by BOTH Email and Name
let rowIndex = -1;
if (umData.length > 1) {
rowIndex = umData.slice(1).findIndex(r => {
const rowEmail = String(r[emailIdx]).trim().toLowerCase();
const rowName = kNorm(r[nameIdx] || '');
return rowEmail === searchEmail && rowName === searchName;
});
}
let attendance = [];
if (form.att_daily) attendance.push('Daily');
if (form.att_wkd) attendance.push('Weekends');
if (form.att_lf) attendance.push('Layali Fadela');
if (form.att_lq) attendance.push('Lailatul Qadr');
const attStr = attendance.join(', ');
// Ensure we don't wipe formulas in Name, FName, MName, LName
const existingRow = (rowIndex > -1) ? umData[rowIndex + 1] : rawHeaders.map(() => '');
const newRow = rawHeaders.map((header, i) => {
const h = String(header).trim().toLowerCase();
if (h === 'email') return form.email;
// AUTO-FILL from Data Logic (User Request)
if (h === 'fullname') return dataTabUser.FullName || dataTabUser.fullname || dataTabUser.Name || dataTabUser.name || '';
if (h === 'name') return dataTabUser.Name || dataTabUser.name || ''; // Auto-fill Name
if (h === 'hofids') return dataTabUser.HOFIDs || dataTabUser.hofids || ''; // Auto-fill HOFIDs
if (h === 'phone') return dataTabUser['Cell#'] || dataTabUser.phone || '';
// RULE: Preserve these columns (filled via formula) - REMOVED 'name' from list
if (['fname','mname','lname'].includes(h)) return existingRow[i];
if (h === 'attendance') return attStr;
if (h === 'chair') return form.chair;
if (String(header).trim() === 'Farzando=<awwala' || h === 'farzando') return form.farzando;
if (h.includes('names(age)') && !h.includes('dikri')) return form.farzando_details || '';
if (String(header).trim() === 'Dikrio=>sania(non-misaq)' || h === 'dikrio') return form.dikrio;
if (h.includes('dikrinames')) return form.dikrio_details || '';
if (h === 'jaali') return form.jaali;
if (h === 'mehmaan') return form.mehmaan;
if (h === 'mhname') return form.mehmaan_name || '';
if (h === 'mhemail') return form.mehmaan_email || '';
if (h === 'mhphone') return form.mehmaan_phone || '';
return '';
});
if (rowIndex > -1) {
umSheet.getRange(rowIndex + 2, 1, 1, newRow.length).setValues([newRow]);
} else {
const lastRow = umSheet.getLastRow();
let tRow = lastRow + 1;
// Finding first truly empty row to avoid gaps
const colData = umSheet.getRange(1, emailIdx + 1, lastRow || 1).getValues();
for (let i = (lastRow || 1) - 1; i >= 0; i--) {
if (String(colData[i][0]).trim() !== '') { tRow = i + 2; break; }
}
if (tRow > umSheet.getMaxRows()) umSheet.insertRowAfter(umSheet.getMaxRows());
umSheet.getRange(tRow, 1, 1, newRow.length).setValues([newRow]);
}
syncAvailableCapacity(form.email, form.jaali, dataTabUser, attStr);
return loginUser(form.email);
}
function getUserFromDataTab(email, uid) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const { rows, headers } = fetchTabRows(ss, TABS.DATA);
// Try UID first (direct lookup)
if (uid) {
const user = rows.find(r => r._rowNum === Number(uid));
if (user) return user;
}
// Fallback to email
const searchEmail = String(email).trim().toLowerCase();
const rawHeaders = headers;
const emailProp = rawHeaders.find(h => String(h).trim().toLowerCase() === 'email');
const user = rows.find(r => String(r[emailProp]).trim().toLowerCase() === searchEmail);
return user || {};
}
/**
* RECONCILIATION: Rebuilds AvailableCapacity from scratch using UserMaster data.
* Useful for testing and manual data cleanup.
*/
function syncAllAvailableCapacity() {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
// 1. Get UserMaster rows
const { rows: umRows, headers: umHeaders } = fetchTabRows(ss, TABS.USER_MASTER);
if (umRows.length === 0) return "UserMaster is empty.";
// 2. Filter for Jaali: Yes
const jaaliUsers = umRows.filter(r => {
const jaaliVal = String(r.Jaali || r.jaali || '').trim().toLowerCase();
return jaaliVal === 'yes';
});
// 3. Get AvailableCapacity Sheet & Headers
const capSheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const capAll = capSheet.getDataRange().getValues();
const capHIdx = findHeaderRow(capAll);
const capRawHeaders = capAll[capHIdx];
const currentCapRows = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY).rows;
const capHeaders = capRawHeaders.map(h => String(h).trim().toLowerCase());
Logger.log(`Found ${jaaliUsers.length} Jaali users in UserMaster.`);
// 4. Transform Jaali users into AvailableCapacity rows
const newCapRows = jaaliUsers.map(u => {
const email = (u.Email || u.email || '').toLowerCase();
const existing = currentCapRows.find(e => (e.Email || e.email || '').toLowerCase() === email);
return capRawHeaders.map((h, i) => {
const head = String(h).trim().toLowerCase();
// Sync Requested Fields
if (head === 'fullname') return u.FullName || u.fullname || '';
if (head === 'name') return u.Name || u.name || '';
if (head === 'email') return u.Email || u.email || '';
if (head === 'phone') return u.Phone || u.phone || u['Cell#'] || '';
if (head === 'attendance' || head === 'attendancemode') return u.Attendance || u.attendance || '';
if (head === 'hofids') return u.HOFIDs || u.hofids || ''; // Added HOFIDs
const cleanKey = String(h).trim();
const lowerKey = cleanKey.toLowerCase();
if (lowerKey.includes('admin') || lowerKey.includes('used') || lowerKey.includes('left') || lowerKey.includes('quota') || lowerKey === 'preflock' || lowerKey === 'notes') {
return (existing && (existing[cleanKey] !== undefined || existing[lowerKey] !== undefined)) ? (existing[cleanKey] ?? existing[lowerKey]) : '';
}
return '';
});
});
// 5. Clear old data and write new
const lastRow = capSheet.getLastRow();
if (lastRow > capHIdx + 1) {
capSheet.getRange(capHIdx + 2, 1, lastRow - capHIdx - 1, capRawHeaders.length).clearContent();
}
if (newCapRows.length > 0) {
capSheet.getRange(capHIdx + 2, 1, newCapRows.length, capRawHeaders.length).setValues(newCapRows);
}
Logger.log(`Synced ${newCapRows.length} users to AvailableCapacity.`);
// 6. Recalculate Quotas
if (typeof recalculateQuotas === 'function') {
recalculateQuotas(capSheet, capRawHeaders);
}
return `Synced ${newCapRows.length} users to AvailableCapacity.`;
}
/**
* Diagnostic/Test runner for the full sync
*/
function testSyncAll() {
try {
const result = syncAllAvailableCapacity();
Logger.log(result);
return result;
} catch (e) {
Logger.log("Error in syncAll: " + e.message);
throw e;
}
}
function syncAvailableCapacity(email, jaali, userData, attendanceStr) {
Logger.log('=== syncAvailableCapacity START ===');
Logger.log('Email: ' + email);
Logger.log('Jaali: ' + jaali);
Logger.log('UserData: ' + JSON.stringify(userData));
Logger.log('Attendance: ' + attendanceStr);
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const capSheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
if (!capSheet) {
Logger.log('ERROR: AvailableCapacity sheet not found!');
return;
}
const { rows, headers: rawHeaders } = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
Logger.log('Headers: ' + JSON.stringify(rawHeaders));
Logger.log('Existing rows count: ' + rows.length);
const searchEmail = String(email).trim().toLowerCase();
const existing = rows.find(r => {
const emailHead = rawHeaders.find(h => String(h).toLowerCase().trim() === 'email');
return String(r[emailHead]).trim().toLowerCase() === searchEmail;
});
const rowIndex = existing ? existing._rowNum : -1;
Logger.log('Existing row index: ' + rowIndex);
const existingRowValues = (rowIndex > -1) ? capSheet.getRange(rowIndex, 1, 1, rawHeaders.length).getValues()[0] : rawHeaders.map(() => '');
const jaaliLower = String(jaali).trim().toLowerCase();
Logger.log('Jaali (normalized): "' + jaaliLower + '"');
if (jaaliLower === 'yes') {
Logger.log('User is eligible (Jaali=Yes) - Adding to AvailableCapacity');
const newRowData = rawHeaders.map((h, i) => {
const head = String(h).trim().toLowerCase();
if (head === 'fullname') return userData.FullName || userData.fullname || '';
if (head === 'name') return userData.Name || userData.name || '';
if (head === 'email') return email;
if (head === 'phone') return userData['Cell#'] || userData.phone || '';
if (head === 'attendance') return attendanceStr || '';
if (head === 'hofids') return userData.HOFIDs || userData.hofids || ''; // Added HOFIDs
const val = (existingRowValues[i] !== undefined && existingRowValues[i] !== null) ? existingRowValues[i] : '';
return val;
});
Logger.log('New row data: ' + JSON.stringify(newRowData));
if (rowIndex > -1) {
Logger.log('Updating existing row at index: ' + rowIndex);
capSheet.getRange(rowIndex, 1, 1, newRowData.length).setValues([newRowData]);
Logger.log('Row updated successfully');
} else {
Logger.log('Adding new row');
// Find first empty row to avoid gaps
const emailIdx = rawHeaders.findIndex(h => String(h).trim().toLowerCase() === 'email');
const checkCol = (emailIdx > -1) ? emailIdx + 1 : 1; // 1-based column index
const lastRow = capSheet.getLastRow();
let targetRow = lastRow + 1;
Logger.log('Last row: ' + lastRow + ', Check column: ' + checkCol);
// Scan for empty slot
const colData = capSheet.getRange(1, checkCol, lastRow > 0 ? lastRow : 1).getValues();
for (let r = 1; r < colData.length; r++) {
if (String(colData[r][0]).trim() === '') {
targetRow = r + 1;
Logger.log('Found empty slot at row: ' + targetRow);
break;
}
}
Logger.log('Writing to row: ' + targetRow);
capSheet.getRange(targetRow, 1, 1, newRowData.length).setValues([newRowData]);
Logger.log('New row added successfully at row ' + targetRow);
}
Logger.log('Calling recalculateQuotas...');
if (typeof recalculateQuotas === 'function') {
recalculateQuotas(capSheet, rawHeaders);
Logger.log('recalculateQuotas completed');
} else {
Logger.log('WARNING: recalculateQuotas function not found');
}
} else if (rowIndex > -1) {
Logger.log('User is NOT eligible (Jaali != Yes) - Removing from AvailableCapacity');
capSheet.deleteRow(rowIndex);
Logger.log('Row deleted at index: ' + rowIndex);
if (typeof recalculateQuotas === 'function') {
recalculateQuotas(capSheet, rawHeaders);
Logger.log('recalculateQuotas completed after deletion');
}
} else {
Logger.log('User is NOT eligible (Jaali != Yes) and not in AvailableCapacity - No action needed');
}
Logger.log('=== syncAvailableCapacity END ===');
}
<!DOCTYPE html>
<!-- VERSION: V4.9.0 (Refactored Codebase - Extracted CSS & Preferences) -->
<html>
<head>
<base target="_top">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<?!= include('styles'); ?>
<script>
/*
REMOVED SAFETY TIMEOUT (4s)
To prevent "Refresh -> Login Page" loop on slow connections.
*/
/* Modal Logic */
function showModal(title, message, type) {
document.getElementById('ui-modal-title').innerText = title || 'Notification';
const body = document.getElementById('ui-modal-body');
if (body) body.innerHTML = message;
const overlay = document.getElementById('ui-modal-overlay');
if (overlay) overlay.classList.remove('hidden');
// Optional: Color code title based on type
const titleEl = document.getElementById('ui-modal-title');
if (titleEl) {
if (type === 'error') titleEl.style.color = '#D32F2F';
else if (type === 'success') titleEl.style.color = '#388E3C';
else titleEl.style.color = 'var(--color-primary)';
}
}
function closeModal() {
const overlay = document.getElementById('ui-modal-overlay');
if (overlay) overlay.classList.add('hidden');
const confirmBtn = document.getElementById('modal-confirm-btn');
if (confirmBtn) confirmBtn.classList.add('hidden');
}
function showConfirm(title, message, onConfirm) {
showModal(title, message, 'info');
const confirmBtn = document.getElementById('modal-confirm-btn');
if (confirmBtn) {
confirmBtn.classList.remove('hidden');
confirmBtn.onclick = function () {
closeModal();
onConfirm();
};
}
}
let currentUser = null;
let adminDashboardUrl = '';
var urlParams = {};
window.onload = function () {
// Populate urlParams from window.location
window.location.search.substring(1).split('&').forEach(pair => {
const [k, v] = pair.split('=');
if (k) urlParams[k] = decodeURIComponent(v || '');
});
// EMERGENCY RESET: If ?reset=true is in URL, clear everything and show login
if (urlParams.reset === 'true') {
localStorage.removeItem('shehrullah_email');
sessionStorage.removeItem('shehrullah_pref_view');
currentUser = null;
console.log('Emergency reset triggered - cleared all saved data');
// Remove reset param from URL
window.history.replaceState({}, document.title, window.location.pathname);
return; // Don't auto-login
}
// LOGOUT: If ?logout=true is in URL, show Shukran page
if (urlParams.logout === 'true') {
localStorage.removeItem('shehrullah_email');
sessionStorage.removeItem('shehrullah_pref_view');
currentUser = null;
console.log('Logout triggered - showing Shukran page');
// Show Shukran view
switchView('view-shukran');
// Remove logout param from URL
window.history.replaceState({}, document.title, window.location.pathname);
return; // Don't auto-login
}
// CRITICAL: Initialize views to correct state FIRST
// Ensure login view is visible and other views are hidden on page load
console.log('Initializing views on page load');
const viewLogin = document.getElementById('view-login');
const viewDash = document.getElementById('view-dashboard');
const viewShukran = document.getElementById('view-shukran');
// Default state: show login, hide everything else
if (viewLogin) viewLogin.classList.remove('hidden');
if (viewDash) viewDash.classList.add('hidden');
if (viewShukran) viewShukran.classList.add('hidden');
const savedEmail = localStorage.getItem('shehrullah_email');
const emailInput = document.getElementById('email');
const form = document.getElementById('login-form'); // CRITICAL: Define here, not just inside doLogin
const loader = document.getElementById('loader'); // CRITICAL: Define here, not just inside doLogin
// Handle email from URL params (for Admin -> User dashboard navigation)
if (urlParams.email && emailInput) {
console.log("Email from URL params:", urlParams.email);
emailInput.value = urlParams.email;
// Clean URL to avoid leaking email in address bar
window.history.replaceState({}, document.title, window.location.pathname + (urlParams.forceUser ? '?forceUser=true' : ''));
}
// 0. CHECK SAVED STATE FIRST
const emailToUse = urlParams.email || savedEmail;
// Sanitization
const isValidEmail = (emailToUse && emailToUse !== 'undefined' && emailToUse !== 'null');
if (isValidEmail && emailInput) {
// AUTO-LOGIN PATH: Keep form hidden, show loader immediately
emailInput.value = emailToUse;
if (form) form.classList.add('hidden');
if (loader) loader.style.display = 'block';
console.log('Auto-login initiated for:', emailToUse);
// Fail-safe timeout
window.loginTimeoutId = setTimeout(() => {
console.warn('Auto-login timeout - forcing login form');
if (loader) loader.style.display = 'none';
if (form) form.classList.remove('hidden');
}, 7000);
// Add slight delay to ensure DOM is ready? No, run immediately for speed.
// Actually, slight delay helps UI render the loader first before main thread blocks
setTimeout(() => doLogin(), 100);
} else {
// NO SAVED DATA: Show Login Form
console.log('No saved email - showing login form');
if (form) form.classList.remove('hidden');
if (loader) loader.style.display = 'none';
}
};
// Clear any auto-login timeouts if they exist (cleanup)
if (window.loginTimeoutId) clearTimeout(window.loginTimeoutId);
function doLogin() {
const emailEl = document.getElementById('email');
const form = document.getElementById('login-form');
const loader = document.getElementById('loader');
if (!emailEl) return;
const email = emailEl.value.trim();
// Clear previous validation errors
const validationError = document.getElementById('email-validation-error');
if (validationError) validationError.style.display = 'none';
// Validate email is not empty
if (!email) {
if (validationError) {
validationError.innerText = 'Please enter your email address';
validationError.style.display = 'block';
}
// CRITICAL: Ensure form is visible if validation fails
if (form) form.classList.remove('hidden');
if (loader) loader.style.display = 'none';
return;
}
// Validate email format
const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) {
if (validationError) {
validationError.innerText = 'Please enter a valid email address (e.g., name@example.com)';
validationError.style.display = 'block';
}
emailEl.focus();
// CRITICAL: Ensure form is visible if validation fails
if (form) form.classList.remove('hidden');
if (loader) loader.style.display = 'none';
return;
}
const errorEl = document.getElementById('login-error');
if (loader) loader.style.display = 'block';
if (errorEl) errorEl.innerText = '';
google.script.run
.withSuccessHandler(res => {
// Clear auto-login timeout if it exists
if (window.loginTimeoutId) {
clearTimeout(window.loginTimeoutId);
window.loginTimeoutId = null;
}
// Save email to localStorage for session persistence
localStorage.setItem('shehrullah_email', email);
onLogin(res);
})
.withFailureHandler(e => {
// Clear auto-login timeout if it exists
if (window.loginTimeoutId) {
clearTimeout(window.loginTimeoutId);
window.loginTimeoutId = null;
}
if (loader) loader.style.display = 'none';
const form = document.getElementById('login-form');
if (form) form.classList.remove('hidden');
if (errorEl) {
google.script.run.withSuccessHandler(adminHtml => {
errorEl.innerHTML = `<div style="color: #d32f2f; margin-top: 20px; text-align: left; background: #ffebeb; padding: 15px; border-radius: 8px; font-size: 0.9rem;">
<b>Login Error:</b> ${e.message}
<br><br>
Please contact an Admin for assistance:<br>
${adminHtml || 'Admin contact info unavailable.'}
</div>`;
}).getAdminContacts();
}
})
.loginUser(email);
}
function onLogin(res) {
const loader = document.getElementById('loader');
if (loader) loader.style.display = 'none';
try {
currentUser = res;
console.log("DASHBOARD_DATA:", res);
if (res.version) {
console.log("VERSION_ACTIVE:", res.version);
}
if (res.isAdmin) {
const isAlreadyOnDash = (document.getElementById('view-dashboard').classList.contains('hidden') === false);
const hasForcedUser = (urlParams && (urlParams.forceUser === 'true' || urlParams.forceUser === true));
// Check Session Storage for preference (Independent of current view state)
const preferredView = sessionStorage.getItem('shehrullah_pref_view');
if (hasForcedUser) {
// URL explicitly says to force user dashboard
console.log('forceUser=true detected, calling showUserDashboard()');
showUserDashboard();
} else if (preferredView === 'user') {
// Persistence: User was previously viewing User Dashboard -> Restore it
console.log('restoring user dashboard preference');
initDashboard(); // Admin viewing as User
} else if (preferredView === 'user') {
// Persistence: User was previously viewing User Dashboard -> Restore it
console.log('restoring user dashboard preference');
initDashboard(); // Admin viewing as User
} else if (preferredView === 'admin' && res.isAdmin && res.scriptUrl) {
// Persistence: Admin was on Admin Dashboard -> Restore it
console.log('restoring admin dashboard preference');
window.location.href = res.scriptUrl + '?page=AdminDashboard';
} else {
// Fresh login / Admin Choice State
// Clear any old session preference to show admin choice
sessionStorage.removeItem('shehrullah_pref_view');
if (res.scriptUrl) {
adminDashboardUrl = res.scriptUrl + '?page=AdminDashboard';
const choice = document.getElementById('admin-choice');
const loginView = document.getElementById('view-login');
const normalButtons = document.getElementById('normal-buttons');
if (choice) choice.classList.remove('hidden');
// Hide normal login/logout buttons when admin choice is shown
if (normalButtons) normalButtons.style.display = 'none';
// CRITICAL FIX: Ensure the parent container is visible!
if (loginView) loginView.classList.remove('hidden');
} else {
console.error("Script URL missing in login response");
alert("System validation error. Please try logging in again.");
// Ensure login view is visible to retry
switchView('view-login');
}
}
} else {
initDashboard();
}
} catch (e) {
console.error("Critical error during login processing:", e);
showModal("System Error", "Failed to load dashboard data. Please try again.", "error");
// Fallback to login view
switchView('view-login');
const form = document.getElementById('login-form');
if (form) form.classList.remove('hidden');
}
}
function showUserDashboard() {
console.log('showUserDashboard called');
sessionStorage.setItem('shehrullah_pref_view', 'user');
const loginView = document.getElementById('view-login');
const dashView = document.getElementById('view-dashboard');
if (loginView) loginView.classList.add('hidden');
// Ensure dashboard view is visible
if (dashView) {
dashView.classList.remove('hidden');
console.log('Dashboard view should now be visible');
}
// Add error handling
try {
initDashboard();
} catch (err) {
console.error('Dashboard initialization failed:', err);
showModal('Error', 'Failed to load dashboard. Please refresh the page.', 'error');
if (loginView) loginView.classList.remove('hidden');
if (dashView) dashView.classList.add('hidden');
}
}
// Safety fallback: If page is blank after 8 seconds, force login view
// REMOVED BY ANTIGRAVITY: This might be causing the refresh loop if loading takes longer or if logic is racy.
// Let's rely on the Loader -> Login Error path instead.
/*
setTimeout(function () {
const dash = document.getElementById('view-dashboard');
const login = document.getElementById('view-login');
const shukran = document.getElementById('view-shukran');
// Check if all are hidden (with null checks)
if (dash && login && shukran &&
dash.classList.contains('hidden') &&
login.classList.contains('hidden') &&
shukran.classList.contains('hidden')) {
console.warn("Blank page detected. Forcing login view.");
switchView('view-login');
}
}, 8000);
*/
function openAdminLink() {
// Open in SAME WINDOW as requested
if (!adminDashboardUrl) {
google.script.run.withSuccessHandler(u => {
window.location.href = u + '?page=AdminDashboard';
}).getScriptUrl();
} else {
window.location.href = adminDashboardUrl;
}
}
function switchView(viewId) {
console.log('switchView called with:', viewId);
// Hide all views
const views = ['view-login', 'view-dashboard', 'view-shukran'];
views.forEach(id => {
const el = document.getElementById(id);
if (el) {
el.classList.add('hidden');
console.log('Hiding:', id);
}
});
// Show the requested view
const targetView = document.getElementById(viewId);
if (targetView) {
targetView.classList.remove('hidden');
console.log('Showing:', viewId);
} else {
console.error('Target view not found:', viewId);
// Fallback: show login if target doesn't exist
const loginView = document.getElementById('view-login');
if (loginView) loginView.classList.remove('hidden');
}
}
function doLogout() {
console.log('doLogout called');
// Clear user data
localStorage.removeItem('shehrullah_email');
sessionStorage.removeItem('shehrullah_pref_view');
currentUser = null;
// Switch to Shukran view
switchView('view-shukran');
}
function initDashboard() {
try {
// Validate currentUser exists
if (!currentUser || !currentUser.user) {
console.error('initDashboard: currentUser is invalid', currentUser);
throw new Error('User data not available. Please log in again.');
}
switchView('view-dashboard');
const user = currentUser.user;
const userName = user.FullName || user.fullname || user.Name || user.name || "User";
const userRawEmail = user.Email || user.email || "";
const userPhone = user['Cell#'] || user['cell#'] || user.Phone || user.phone || "";
const detailsEl = document.getElementById('user-details');
if (detailsEl) {
detailsEl.innerHTML = `
<div style="font-weight:600; font-size:1.1rem; margin-top:5px; color:#333;">${userName}</div>
<div style="font-size:0.85rem; color:#666;">${userRawEmail} | ${userPhone}</div>`;
}
const adminBtn = document.getElementById('btn-admin-dash');
if (currentUser.isAdmin && adminBtn) adminBtn.classList.remove('hidden');
// 1. Render Quota Stats
try {
console.log('currentUser.userQuotas:', currentUser.userQuotas);
if (currentUser.userQuotas) {
updateDashboardStats('daily', currentUser.userQuotas.daily);
updateDashboardStats('lf', currentUser.userQuotas.lf);
}
} catch (e) { console.error('Error rendering quotas:', e); }
// 2. Render My Bookings
try {
if (currentUser.myBookings) {
renderBookingSummary(currentUser.myBookings, currentUser.seatLabels);
}
} catch (e) { console.error('Error rendering bookings:', e); }
// 3. Auto-Refresh Active Modular Grid
try {
if (window.activeModularType) {
loadSeatSelectionModular(window.activeModularType);
}
} catch (e) { console.error('Error loading modular grid:', e); }
// 4. Render Preferences
try {
const prefs = currentUser.userPreferences;
if (prefs && (prefs.Attendance || prefs.attendance)) {
renderPreferencesInline(prefs);
} else {
renderPreferencesForm({});
}
} catch (e) { console.error('Error rendering prefs:', e); }
// 5. Gate Seat Selection Visibility
try {
const seatNav = document.getElementById('seat-nav-section');
const prefs = currentUser.userPreferences || {}; // Safety fallback
const hasJaali = (prefs && (prefs.Jaali === 'Yes' || prefs.jaali === 'Yes'));
const hasActivePhase = currentUser.activePhase && currentUser.activePhase.trim() !== '';
// LF VISIBILITY CHECK (User Side)
const lfUserSection = document.getElementById('user-section-lf');
const activePhaseNum = parseInt(currentUser.activePhase) || 0;
if (lfUserSection) {
if (activePhaseNum < 2) {
lfUserSection.classList.add('hidden');
} else {
lfUserSection.classList.remove('hidden');
}
}
if (seatNav) {
if (hasJaali && hasActivePhase) {
seatNav.classList.remove('hidden');
} else {
seatNav.classList.add('hidden');
}
}
} catch (e) { console.error('Error gating visibility:', e); }
// 6. Render Family Members
try {
const famSec = document.getElementById('family-section');
const list = document.getElementById('family-list');
if (currentUser.isPrimary && currentUser.familyMembers && currentUser.familyMembers.length > 0) {
// ... existing family render logic ...
// We need to keep the inner logic intact or it's too much code to replace blindly.
// Let's rely on the outer try-catch for the complex family logic for now,
// OR better: copy the family logic since I don't want to break it.
// Actually, the ReplacementChunk needs to match EXACT text.
// Since I cannot see the full family logic in the view_file output above (it was truncated?),
// I should be careful.
// The view_file output showed lines 500-581. I can replace that block safely.
renderFamilySectionSafe(famSec, list, currentUser);
} else {
if (famSec) famSec.classList.add('hidden');
}
} catch (e) { console.error('Error rendering family:', e); }
} catch (error) {
console.error('Dashboard initialization failed:', error);
// Clear saved email and show login
localStorage.removeItem('shehrullah_email');
currentUser = null;
const loader = document.getElementById('loader');
if (loader) loader.style.display = 'none';
const form = document.getElementById('login-form');
if (form) form.classList.remove('hidden');
switchView('view-login');
showModal('Dashboard Error',
'Failed to load dashboard: ' + error.message + '<br><br>Please log in again.',
'error');
}
}
function updateDashboardStats(typeKey, qData) {
const box = document.getElementById('dash-stats-' + typeKey);
if (!box) {
console.warn('updateDashboardStats: box not found for', typeKey);
return;
}
if (!qData) {
console.warn('updateDashboardStats: no quota data for', typeKey);
return;
}
console.log('updateDashboardStats:', typeKey, qData);
// Handle multiple possible data structures
// Try different property name variations (case-insensitive)
const lim = qData.limit ?? qData.Limit ?? qData.dailyquota ?? qData.DailyQuota ?? qData.lfquota ?? qData.LFQuota ?? qData.quota ?? qData.Quota ?? 0;
const usd = qData.used ?? qData.Used ?? qData.dailyused ?? qData.DailyUsed ?? qData.lfused ?? qData.LFUsed ?? 0;
const lft = qData.left ?? qData.Left ?? qData.dailyleft ?? qData.DailyLeft ?? qData.lfleft ?? qData.LFLeft ?? (lim - usd); // Calculate if not provided
const limitEl = box.querySelector('.stat-limit');
const usedEl = box.querySelector('.stat-used');
const leftEl = box.querySelector('.stat-left');
if (limitEl) limitEl.innerText = lim;
if (usedEl) usedEl.innerText = usd;
if (leftEl) {
leftEl.innerText = lft;
if (lft <= 0) leftEl.style.color = 'red';
else leftEl.style.color = 'var(--color-accent)';
}
}
function renderBookingSummary(bookings, labels) {
const legendBox = document.getElementById('dashboard-seat-legend');
const dailyContainer = document.getElementById('booking-list-daily');
const lfContainer = document.getElementById('booking-list-lf');
const dailyBox = document.getElementById('table-daily-container');
const lfBox = document.getElementById('table-lf-container');
// 1. Dynamic Legend Logic
if (labels && legendBox) {
const usedCodes = new Set();
bookings.forEach(b => {
// Robust match: Extract leading letters (e.g. "BRHN" from "BRHN-12" or "BRHN12")
const match = String(b.seat).match(/^([A-Z]+)/i);
const code = match ? match[1].toUpperCase() : '';
if (labels[code]) {
usedCodes.add(code);
}
});
const legendParts = [];
// Use the order in 'labels' but only include used ones
for (let code in labels) {
if (usedCodes.has(code)) {
legendParts.push(`<b>${code}</b>: ${labels[code]}`);
}
}
if (legendParts.length > 0) {
legendBox.innerHTML = `<b>Legend: </b> ` + legendParts.join(' | ');
legendBox.style.display = 'block';
legendBox.style.marginBottom = '10px';
} else {
legendBox.style.display = 'none';
}
}
// 2. Filter Bookings
const dailyBookings = bookings.filter(b => b.type === 'Daily');
const lfBookings = bookings.filter(b => b.type === 'LF');
// 3. Render Helper (Updated for Bulk Delete)
const renderTable = (list, container, parentBox, typeTag) => {
if (!container || !parentBox) return;
// Clear previous content including button if it exists
container.innerHTML = '';
if (list.length === 0) {
container.innerHTML = '<div style="font-size:0.85rem; color:#999; font-style:italic; padding:10px;">No seats selected yet.</div>';
parentBox.classList.remove('hidden');
// Hide Delete Button if it acts globally, or ensure we don't append it
return;
}
parentBox.classList.remove('hidden');
// Create Master Control Div
const controlDiv = document.createElement('div');
controlDiv.style.marginBottom = "5px";
controlDiv.style.textAlign = "right";
const deleteBtn = document.createElement('button');
deleteBtn.className = "btn";
deleteBtn.style.background = "#FFE5E5";
deleteBtn.style.color = "#D32F2F";
deleteBtn.style.fontSize = "0.75rem";
deleteBtn.style.padding = "4px 8px";
deleteBtn.style.border = "none";
deleteBtn.style.borderRadius = "4px";
deleteBtn.style.display = "none"; // Hidden by default
deleteBtn.innerText = "Delete Selected";
deleteBtn.onclick = () => confirmBulkDelete(typeTag, list);
controlDiv.appendChild(deleteBtn);
container.appendChild(controlDiv);
let html = `
<div class="table-container" style="overflow-x:auto;">
<table style="width:100%; min-width:300px; margin:0 auto; border-collapse:collapse; background:#fff; font-size:0.85rem; border:1px solid #eee;">
<thead style="background:#f9f9f9; position:sticky; top:0; z-index:10;">
<tr>
<th style="text-align:left; padding:8px; border-bottom:2px solid #eee; width:50%;">Date</th>
<th style="text-align:left; padding:8px; border-bottom:2px solid #eee; width:35%;">Seat</th>
<th style="text-align:center; padding:8px; border-bottom:2px solid #eee; width:15%;">
<!-- Select All Header Logic -->
</th>
</tr>
</thead>
<tbody>`;
list.forEach((b, idx) => {
// Unique ID for Checkbox
const chkId = `chk-${typeTag}-${idx}`;
html += `
<tr>
<td style="padding:8px; border-bottom:1px solid #eee; white-space:nowrap;">
<label for="${chkId}" style="cursor:pointer; display:block;">${b.label}</label>
</td>
<td style="padding:8px; border-bottom:1px solid #eee;"><b>${b.seat}</b></td>
<td style="padding:8px; border-bottom:1px solid #eee; text-align:center;">
<input type="checkbox" id="${chkId}" class="chk-delete-${typeTag}"
data-date="${b.date}" data-seat="${b.seat}"
style="transform:scale(1.2); cursor:pointer;"
onchange="toggleDeleteButton('${typeTag}')">
</td>
</tr>
`;
});
html += `</tbody></table></div>`;
const tableWrapper = document.createElement('div');
tableWrapper.innerHTML = html;
container.appendChild(tableWrapper);
};
// 4. Execute Render
renderTable(dailyBookings, document.getElementById('booking-list-daily'), document.getElementById('table-daily-container'), 'Daily');
renderTable(lfBookings, document.getElementById('booking-list-lf'), document.getElementById('table-lf-container'), 'LF');
// 5. Update Button Text (Keep existing login)
const btnDaily = document.getElementById('btn-action-daily');
if (btnDaily) {
if (dailyBookings.length > 0) {
btnDaily.innerText = "Modify Daily Jagah";
} else {
btnDaily.innerText = "Select Daily Jagah";
}
btnDaily.onclick = function () {
loadSeatSelectionModular('Daily');
};
}
const btnLF = document.getElementById('btn-action-lf');
if (btnLF) {
const currentPhase = parseInt(currentUser.activePhase || '0');
const isLFAllowed = currentPhase >= 2;
if (lfBookings.length > 0) {
btnLF.innerText = "Modify LF Jagah";
btnLF.disabled = !isLFAllowed;
} else {
btnLF.innerText = isLFAllowed ? "Select LF Jagah" : "Select LF Jagah (Starts Phase 2)";
btnLF.disabled = !isLFAllowed;
}
if (!isLFAllowed) {
btnLF.style.opacity = '0.6';
btnLF.style.cursor = 'not-allowed';
btnLF.title = "Layali Fadela selection starts in Phase 2";
} else {
btnLF.style.opacity = '1';
btnLF.style.cursor = 'pointer';
btnLF.title = "";
}
btnLF.onclick = function () {
if (!isLFAllowed) {
showModal("Not Available Yet", "Layali Fadela seat selection will begin with Phase 2.", "info");
return;
}
loadSeatSelectionModular('LF');
};
}
}
function toggleDeleteButton(typeTag) {
// Find if any checkbox is checked
const anyChecked = document.querySelector(`.chk-delete-${typeTag}:checked`);
// Find the button (It's the first child of the container in my render logic, but need to be robust)
// Actually, the button is inside 'controlDiv' which is the first child of container.
const containerId = (typeTag === 'Daily') ? 'booking-list-daily' : 'booking-list-lf';
const container = document.getElementById(containerId);
if (container) {
const btn = container.querySelector('button');
if (btn) {
btn.style.display = anyChecked ? 'inline-block' : 'none';
btn.innerText = "Delete Selected";
}
}
}
function confirmBulkDelete(typeTag, fullList) {
const chks = document.querySelectorAll(`.chk-delete-${typeTag}:checked`);
const valid = [];
const locked = [];
const now = new Date();
// 1. Validate 48h Lock
for (let chk of chks) {
const dStr = chk.getAttribute('data-date');
const seat = chk.getAttribute('data-seat');
let isLocked = false;
try {
const d = new Date(dStr);
if (!isNaN(d.getTime())) {
const diffHrs = (d.getTime() - now.getTime()) / (1000 * 60 * 60);
if (diffHrs < 48) isLocked = true;
}
} catch (e) { console.warn("Date parse error", e); }
if (isLocked) locked.push({ date: dStr, seat: seat });
else valid.push({ date: dStr, seat: seat });
}
// Case A: Everything is locked
if (valid.length === 0 && locked.length > 0) {
showModal("Seat Locked",
`To help us plan better, seats become locked and cancellations are disabled during the 48 hours of the selected date.<br><br><b>Locked Seats:</b><br>${locked.map(l => l.seat + ' (' + l.date + ')').join('<br>')}`,
"error");
return;
}
// Case B: Nothing selected
if (valid.length === 0) return;
// Case C: Mixed or All Valid
let msg = `Are you sure you want to delete these ${valid.length} seats?`;
if (locked.length > 0) {
msg += `<br><br><div style="background:#fff3cd; padding:10px; border-left:4px solid #ffc107; text-align:left; font-size:0.9rem;">
<b>Warning:</b> ${locked.length} seat(s) are within the 48-hour lock period and will <u>NOT</u> be deleted:
<ul style="margin:5px 0 0 15px; padding:0;">${locked.map(l => '<li>' + l.date + '</li>').join('')}</ul>
</div>`;
}
// Valid List
let listHtml = `<ul style="text-align:left; max-height:150px; overflow-y:auto; margin:10px 0; padding-left:20px;">`;
valid.forEach(s => {
listHtml += `<li><b>${s.date}</b>: ${s.seat}</li>`;
});
listHtml += `</ul>`;
msg += `<br><b>Deleting:</b>${listHtml}`;
showConfirm("Confirm Deletion", msg, function () {
const email = (currentUser.user.Email || currentUser.user.email);
google.script.run
.withSuccessHandler(res => {
if (res.success) {
// Partial success message if we had locked items?
// The backend only removed valid ones.
let successMsg = `Deleted ${res.count} seats successfully.`;
if (locked.length > 0) {
successMsg += `<br><br><span style="color:#e67e22; font-size:0.85rem;">(${locked.length} locked seats were skipped)</span>`;
}
showModal("Success", successMsg, "success");
doLogin(); // Refresh
} else {
showModal("Error", res.message, "error");
}
})
.withFailureHandler(e => showModal("System Error", e.message, "error"))
.cancelBookingsBatch(email, typeTag, valid);
});
}
// Keep legacy simple cancel for compatibility or remove?
// User asked to replace it. So I removed the cancelFromDash function call from HTML generation above.
// But I will leave the definition here just in case, or overwrite it.
// Actually, I am overwriting the renderTable closure which used it.
// The cancelFromDash function definition (lines 778-795) is separate. I can leave it or remove it.
// Since I'm replacing lines 689-775, I'll just leave cancelFromDash below line 796 untouched but unused.
// ADDED BY ANTIGRAVITY: Missing function causing crash
function renderFamilySectionSafe(container, list, currentUser) {
if (!container || !list) return;
container.classList.remove('hidden');
list.innerHTML = '';
const members = currentUser.familyMembers || [];
if (members.length === 0) {
container.classList.add('hidden');
return;
}
let html = '';
members.forEach((fam, idx) => {
// Robust data access
const name = fam.Name || fam.name || fam.FullName || fam.fullname || 'Family Member';
const hasId = (fam.EJamaatID || fam.ejamaatid) ? true : false;
const idDisplay = hasId ? `(${fam.EJamaatID || fam.ejamaatid})` : '';
// Calculate status? For now simple list.
// If we have booking info in 'fam', use it.
// But currentUser.familyMembers usually just has profile data.
// The booking summary logic elsewhere handles their seats.
html += `<li style="padding:8px 0; border-bottom:1px solid #eee;">
<div style="font-weight:500;">${name} <span style="font-size:0.8rem; color:#888;">${idDisplay}</span></div>
</li>`;
});
list.innerHTML = html;
}
function cancelFromDash(type, date, seat) {
showConfirm("Confirm Cancellation", `Are you sure you want to cancel ${type} seat (${seat}) on ${date}?`, function () {
const email = (currentUser.user.Email || currentUser.user.email);
showModal("Cancelling...", "Please wait.", "info");
google.script.run
.withSuccessHandler(res => {
if (res.success) {
showModal("Cancelled", "Seat successfully released.", "success");
doLogin(); // Refresh dashboard
} else {
showModal("Error", res.message, "error");
}
})
.withFailureHandler(e => showModal("System Error", e.message, "error"))
.cancelBooking(email, type, date, seat);
});
}
function renderFamilySectionSafe(famSec, list, currentUser) {
famSec.classList.remove('hidden');
list.innerHTML = '';
let foundOthersCount = 0;
const myUid = Number(currentUser.user.uid);
// Update Phase Schedule Display
if (currentUser.phaseSchedule) {
const fmt = (d) => {
if (!d) return 'To be announced';
try {
const date = new Date(d);
if (isNaN(date.getTime())) return d; // Return original text if not a valid date
return date.toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
} catch (e) { return d; }
};
const p1 = document.getElementById('sched-p1');
const p2 = document.getElementById('sched-p2');
const p3 = document.getElementById('sched-p3');
if (p1) p1.innerText = fmt(currentUser.phaseSchedule.p1);
if (p2) p2.innerText = fmt(currentUser.phaseSchedule.p2);
if (p3) p3.innerText = fmt(currentUser.phaseSchedule.p3);
}
currentUser.familyMembers.forEach(mem => {
if (Number(mem.uid) === myUid) return;
foundOthersCount++;
let statusString = '';
if (mem.isEligible) {
if (mem.preferences) {
const fullPrefStr = getFormattedPreferences(mem.preferences);
statusString = `<div class="prefs" style="margin-top:5px; padding:8px; background:#f9f9f9; border-radius:6px; font-size:0.85rem;">${fullPrefStr}</div>`;
// Only show seat status if they're eligible for Jaali (Chair=No AND Farzando=No)
const p = mem.preferences;
const chair = (p.Chair || p.chair || '').toLowerCase();
const farzando = (p['Farzando=<awwala'] || p.Farzando || p.farzando || '').toLowerCase();
const jaali = (p.Jaali || p.jaali || '').toLowerCase();
const isJaaliEligible = (chair === 'no' && farzando === 'no') || jaali === 'yes';
if (isJaaliEligible) {
// NEW LOGIC: Only show pending status if a phase is actually active!
const isPhaseActive = currentUser.activePhase && currentUser.activePhase.trim() !== '';
const s = mem.seatStatus || { hasSeat: false };
if (s.hasSeat) {
statusString += `<div class="status-success">Daily Seats selected</div>`;
} else if (isPhaseActive) {
statusString += `<div class="status-pending">Seat selection pending</div>`;
}
}
} else {
statusString = '<div class="status-pending">Preferences not filled</div>';
}
}
const div = document.createElement('div');
div.className = 'family-card';
div.innerHTML = `
<div class="name">${mem.FullName || mem.fullname || mem.Name || mem.name || 'User'}</div>
<div style="font-size:0.8rem; color:#777;">${mem.Email} | ${mem['Cell#']}</div>
${statusString}
`;
list.appendChild(div);
});
if (foundOthersCount === 0) famSec.classList.add('hidden');
}
</script>
<?!= include('preferences'); ?>
<script>
function switchView(id) {
console.log("Switching view to:", id);
['view-login', 'view-dashboard', 'view-shukran'].forEach(v => {
const el = document.getElementById(v);
if (el) el.classList.add('hidden');
});
const target = document.getElementById(id);
if (target) {
target.classList.remove('hidden');
} else {
console.error("View not found:", id);
alert("System Error: View '" + id + "' missing.");
}
}
function doLogout() {
localStorage.removeItem('shehrullah_email');
currentUser = null;
document.getElementById('email').value = '';
const ch = document.getElementById('admin-choice');
if (ch) ch.classList.add('hidden');
switchView('view-shukran');
}
</script>
</head>
<body class="user-layout">
<!-- Login -->
<div id="view-login" class="container">
<!-- Logo Section moved above title -->
<div style="text-align: center; margin-bottom: 20px;">
<img id="login-header-logo" src="https://i.imgur.com/DfSwMGd.png" alt="Header Logo"
style="max-width: 250px; height: auto;">
</div>
<h2 style="text-align: center; margin-bottom: 20px;">Shehrullah Dashboard</h2>
<div id="login-form">
<input type="email" id="email" placeholder="name@example.com" autocomplete="off" required
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
title="Please enter a valid email address (e.g., name@example.com)">
<div id="email-validation-error" style="color: #d32f2f; font-size: 0.85rem; margin-top: 5px; display: none;">
</div>
<!-- Login and Logout buttons side by side (normal state) -->
<div id="normal-buttons" style="display: flex; gap: 10px; justify-content: center; margin-top: 20px;">
<button class="btn btn-primary" onclick="doLogin()">Login</button>
<button class="btn" style="background: transparent; border: 1px solid var(--color-primary); font-size: 0.85rem;"
onclick="doLogout()">Logout</button>
</div>
<!-- Admin choice buttons (hidden by default) -->
<div id="admin-choice" class="hidden"
style="margin-top: 20px; display: flex; gap: 10px; justify-content: center; flex-direction: column;">
<div style="display: flex; gap: 10px;">
<button class="btn btn-primary" style="flex: 1; padding: 10px; font-size: 0.9rem;"
onclick="showUserDashboard()">User Dashboard</button>
<button class="btn"
style="flex: 1; padding: 10px; font-size: 0.9rem; background: var(--color-accent); color: #fff;"
onclick="openAdminLink()">Admin Dashboard</button>
</div>
<!-- Logout button moves here for admin users -->
<button class="btn"
style="background: transparent; border: 1px solid var(--color-primary); font-size: 0.85rem; margin-top: 5px;"
onclick="doLogout()">Logout</button>
</div>
</div>
<div class="loader" id="loader"></div>
<div id="login-error"></div>
</div>
<!-- Dashboard View -->
<div id="view-dashboard" class="hidden fade-in">
<div class="card dashboard-header" style="display:flex; justify-content:space-between; align-items:flex-start;">
<div style="text-align:left;">
<h2 style="margin:0 0 5px 0; color:var(--color-primary);">My Dashboard</h2>
<div id="user-details">
<!-- User info populated by JS -->
</div>
</div>
<div style="text-align:right;">
<button id="btn-admin-dash" class="btn hidden" onclick="openAdminLink()" style="margin-bottom:5px;">Admin
Dashboard</button>
<div>
<button class="btn" style="background:transparent; border:1px solid var(--color-primary);"
onclick="doLogout()">Logout</button>
</div>
</div>
</div>
<div class="pref-section">
<h3>My Preferences</h3>
<div id="my-pref-content"></div>
</div>
<!-- Phase Schedule Information (Moved Here) -->
<div id="phase-schedule-container"
style="background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%); padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 5px solid var(--color-primary); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
<h3
style="margin-top:0; color:var(--color-primary); font-size:1.1rem; border-bottom:1px solid #ddd; padding-bottom:5px;">
Phases and Selection Schedule</h3>
<div id="phase-schedule-list" style="font-size: 0.9rem; line-height: 1.6;">
<div style="margin-bottom:4px;"><strong>Phase 1:</strong> <span id="sched-p1">Loading...</span></div>
<div style="margin-bottom:4px;"><strong>Phase 2:</strong> <span id="sched-p2">Loading...</span> <span
style="color:#d35400; font-weight:600; font-size:0.8rem;">(Layali Fadela selection starts here)</span></div>
<div><strong>Phase 3:</strong> <span id="sched-p3">Loading...</span></div>
</div>
</div>
<!-- ADDED BY ANTIGRAVITY: Seat Selection Nav & Summary (Split View) -->
<div id="seat-nav-section" class="hidden" style="margin-bottom:20px; text-align:center;">
<h3
style="margin-bottom:12px; font-size:1.25rem; color:var(--color-primary); font-weight:700; padding-left:12px; display:inline-block;">
My Jaali Jagah</h3>
<!-- New Legend Container (Dynamic) -->
<div id="dashboard-seat-legend"
style="margin-bottom:15px; font-size:0.85rem; color:#555; background:#f9f9f9; padding:8px; border-radius:6px; border:1px solid #eee; display:none;">
</div>
<!-- Section 1: Daily Jagah -->
<div style="background:#fafafa; border:1px solid #eee; border-radius:12px; padding:20px; margin-bottom:20px;">
<h3
style="margin-bottom:15px; color:#555; border-bottom:1px solid #eee; padding-bottom:5px; font-size:1.05rem; font-weight:600;">
Daily Jagah</h3>
<div
style="display:flex; justify-content:center; align-items:center; flex-wrap:wrap; gap:20px; margin-bottom:15px;">
<!-- Stats Box -->
<div id="dash-stats-daily"
style="background:#f4f7f6; padding:8px 15px; border-radius:15px; border:1px solid #e0e6e4; display:flex; flex-direction:column; align-items:flex-start; gap:5px; box-shadow:0 2px 4px rgba(0,0,0,0.02); width:max-content; min-width:260px;">
<div
style="display:flex; justify-content:space-between; width:100%; align-items:center; border-bottom:1px solid #ddd; padding-bottom:5px;">
<div style="font-size:0.8rem; font-weight:600; color:#666;">Quota</div>
<div style="display:flex; gap:10px; font-size:0.85rem; color:#444; align-items:center;">
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Limit:</span><b class="stat-limit">-</b></div>
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Used:</span><b class="stat-used">-</b></div>
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Left:</span><b class="stat-left"
style="color:var(--color-accent);">-</b></div>
<span style="color:#ccc;">|</span>
</div>
</div>
<div style="width:100%; text-align:center; font-size:0.75rem; color:#888;">
Monthly Limit (all 3 phases)
</div>
</div>
<!-- Action Button -->
<div>
<button id="btn-action-daily" class="btn btn-primary" onclick="loadSeatSelectionModular('Daily')">Select
Daily Jagah</button>
</div>
</div>
<!-- Inline Selection Anchor (Daily) -->
<div id="selection-daily-anchor"></div>
<!-- Selected Seats Table (Daily) -->
<div id="table-daily-container" class="hidden" style="margin-top:10px;">
<h4 style="font-size:0.85rem; color:#666; margin-bottom:5px;">Your Selections:</h4>
<div id="booking-list-daily"></div>
</div>
</div>
<!-- Section 2: LF Jagah -->
<div id="user-section-lf"
style="background:#fafafa; border:1px solid #eee; border-radius:12px; padding:20px; margin-bottom:20px;">
<h3
style="margin-bottom:15px; color:#555; border-bottom:1px solid #eee; padding-bottom:5px; font-size:1.05rem; font-weight:600;">
Layali Fadela Jagah</h3>
<div
style="display:flex; justify-content:center; align-items:center; flex-wrap:wrap; gap:20px; margin-bottom:15px;">
<!-- Stats Box -->
<div id="dash-stats-lf"
style="background:#f4f7f6; padding:8px 15px; border-radius:15px; border:1px solid #e0e6e4; display:flex; flex-direction:column; align-items:flex-start; gap:5px; box-shadow:0 2px 4px rgba(0,0,0,0.02); width:max-content; min-width:260px;">
<div
style="display:flex; justify-content:space-between; width:100%; align-items:center; border-bottom:1px solid #ddd; padding-bottom:5px;">
<div style="font-size:0.8rem; font-weight:600; color:#666;">Quota</div>
<div style="display:flex; gap:10px; font-size:0.85rem; color:#444; align-items:center;">
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Limit:</span><b class="stat-limit">-</b></div>
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Used:</span><b class="stat-used">-</b></div>
<span style="color:#ccc;">|</span>
<div style="display:flex; gap:5px;"><span>Left:</span><b class="stat-left"
style="color:var(--color-accent);">-</b></div>
<span style="color:#ccc;">|</span>
</div>
</div>
<div style="width:100%; text-align:center; font-size:0.75rem; color:#888;">
Monthly Limit (all 3 phases)
</div>
</div>
<!-- Action Button -->
<div>
<button id="btn-action-lf" class="btn btn-primary" onclick="loadSeatSelectionModular('LF')">Select LF
Jagah</button>
</div>
</div>
<!-- Inline Selection Anchor (LF) -->
<div id="selection-lf-anchor"></div>
<!-- Selected Seats Table (LF) -->
<div id="table-lf-container" class="hidden" style="margin-top:10px;">
<h4 style="font-size:0.85rem; color:#666; margin-bottom:5px;">Your Selections:</h4>
<div id="booking-list-lf"></div>
</div>
</div>
</div>
<!-- Modular Seat Selection View Injection -->
<?!= include('seatselect') ?>
<!-- END OF ADDITION -->
<div id="family-section" class="hidden">
<h3>Family Members</h3>
<div id="family-list"></div>
</div>
</div>
<!-- Shukran/Thank You View -->
<div id="view-shukran" class="container hidden fade-in">
<div style="text-align: center; padding: 40px 20px;">
<img src="https://i.imgur.com/DfgOAol.png" alt="Shukran" style="max-width: 400px; width: 100%; height: auto;">
<div style="margin-top: 30px;">
<button class="btn btn-primary" onclick="forceBackToLogin()">Back to Login</button>
</div>
</div>
</div>
<script>
function forceBackToLogin() {
// Clear everything to be safe
localStorage.removeItem('shehrullah_email');
sessionStorage.clear();
// CRITICAL: Reset UI state before reload to prevent "hidden buttons" bug
const normalButtons = document.getElementById('normal-buttons');
const adminChoice = document.getElementById('admin-choice');
const loginForm = document.getElementById('login-form');
if (normalButtons) normalButtons.style.display = 'flex';
if (adminChoice) adminChoice.classList.add('hidden');
if (loginForm) loginForm.classList.remove('hidden');
// Force reload to initial state (use replace to prevent back-button issues)
window.top.location.replace(window.top.location.href.split('?')[0]);
}
</script>
<!-- Reusable Modal (Added by Antigravity) -->
<div id="ui-modal-overlay" class="hidden"
style="position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:1000; display:flex; justify-content:center; align-items:center;">
<div
style="background:#fff; padding:20px; border-radius:12px; width:90%; max-width:400px; box-shadow:0 10px 25px rgba(0,0,0,0.2);">
<h3 id="ui-modal-title" style="margin-top:0; color:var(--color-primary);">Message</h3>
<div id="ui-modal-body" style="margin:15px 0; line-height:1.5; color:#444;"></div>
<div style="text-align:right; display:flex; gap:10px; margin-top:20px;">
<button class="btn btn-primary" style="flex:1;" onclick="closeModal()">Close</button>
<button id="modal-confirm-btn" class="btn hidden"
style="flex:1; background:var(--color-accent); color:#fff;">Confirm Action</button>
</div>
</div>
</div>
<script>
function forceBackToLogin() {
// Clear data but keep localStorage email if possible? No, user requested reset.
localStorage.removeItem('shehrullah_email');
sessionStorage.removeItem('shehrullah_pref_view');
currentUser = null;
// Ensure all views are hidden first
document.querySelectorAll('.container').forEach(el => el.classList.add('hidden'));
// Reset view
const loginView = document.getElementById('view-login');
if (loginView) loginView.classList.remove('hidden');
// Clear forms
const emailInput = document.getElementById('email');
if (emailInput) emailInput.value = '';
const form = document.getElementById('login-form');
if (form) form.classList.remove('hidden');
const loader = document.getElementById('loader');
if (loader) loader.style.display = 'none';
console.log('Force reset back to login executed');
}
</script>
</body>
</html>
<script>
function getFormattedPreferences(data) {
if (!data) return "No preferences saved yet.";
let parts = [];
const getVal = (f1, f2, f3) => data[f1] || data[f2] || data[f3] || '';
const att = getVal('Attendance', 'attendance');
if (att) parts.push(`<b>Attendance:</b> ${att}`);
const chr = getVal('Chair', 'chair');
if (chr) parts.push(`<b>Chair:</b> ${chr}`);
const fVal = getVal('Farzando=<awwala', 'Farzando', 'farzando');
if (fVal) {
let farz = `<b>Farzando:</b> ${fVal}`;
const fDetails = getVal('Names(age)', 'Names(AgE)', 'names(age)');
if (fDetails) farz += ` (${fDetails})`;
parts.push(farz);
}
const dVal = getVal('Dikrio=>sania(non-misaq)', 'Dikrio', 'dikrio');
if (dVal) {
let dik = `<b>Dikrio:</b> ${dVal}`;
const dDetails = getVal('Dikrinames(age)', 'Dikrinames(AgE)', 'dikrinames(age)');
if (dDetails) dik += ` (${dDetails})`;
parts.push(dik);
}
const mVal = getVal('Mehmaan', 'mehmaan');
if (mVal) {
let meh = `<b>Mehmaan:</b> ${mVal}`;
const mName = getVal('MHName', 'mhname');
if (mName) meh += ` (${mName})`;
parts.push(meh);
}
const jal = getVal('Jaali', 'jaali');
if (jal) parts.push(`<b>Jaali:</b> ${jal}`);
return parts.length > 0 ? parts.join(' | ') : "No preferences saved yet.";
}
function renderPreferencesInline(data) {
const container = document.getElementById('my-pref-content');
if (!container) return;
const html = `
<div style="line-height:1.6; margin-bottom:15px; font-size:1rem; color:#333; padding:10px; background:#f9f9f9; border-radius:8px;">
${getFormattedPreferences(data)}
</div>
<div style="text-align: right;">
<button class="btn btn-primary" onclick="editPreferences()">Edit Preferences</button>
</div>
`;
container.innerHTML = html;
}
function editPreferences() {
renderPreferencesForm(currentUser.userPreferences || {});
}
function renderPreferencesForm(data) {
const container = document.getElementById('my-pref-content');
if (!container) return;
const getVal = (f1, f2, f3) => data[f1] || data[f2] || data[f3] || '';
const chk = (f1, f2, val) => (getVal(f1, f2) === val) ? 'checked' : '';
const chkInc = (f1, f2, val) => (String(getVal(f1, f2)).includes(val)) ? 'checked' : '';
const isFarzandoYes = (getVal('Farzando=<awwala', 'Farzando', 'farzando') === 'Yes');
const isDikrioYes = (getVal('Dikrio=>sania(non-misaq)', 'Dikrio', 'dikrio') === 'Yes');
const isMehmaanYes = (getVal('Mehmaan', 'mehmaan') === 'Yes');
const html = `
<form id="prefForm">
<div class="pref-item">
<div class="pref-label">Attendance</div>
<div class="pref-options vertical">
<label><input type="checkbox" name="att_daily" ${chkInc('Attendance', 'attendance', 'Daily')}> Daily</label>
<label><input type="checkbox" name="att_wkd" ${chkInc('Attendance', 'attendance', 'Weekends')}> Weekends</label>
<label><input type="checkbox" name="att_lf" ${chkInc('Attendance', 'attendance', 'Layali Fadela')}> Layali Fadela</label>
<label><input type="checkbox" name="att_lq" ${chkInc('Attendance', 'attendance', 'Lailatul Qadr')}> Lailatul Qadr</label>
</div>
</div>
<div class="pref-item">
<div class="pref-label">Chair</div>
<div class="pref-options">
<label><input type="radio" name="chair" value="Yes" ${chk('Chair', 'chair', 'Yes')} onchange="checkJaaliVisibility()"> Yes</label>
<label><input type="radio" name="chair" value="No" ${chk('Chair', 'chair', 'No')} onchange="checkJaaliVisibility()"> No</label>
</div>
</div>
<div class="pref-item" style="flex-direction: column; align-items: flex-start;">
<div style="display: flex; width: 100%; align-items: flex-start;">
<div class="pref-label">Farzando (<=awwala)</div>
<div class="pref-options">
<label><input type="radio" name="farzando" value="Yes" ${chk('Farzando=<awwala', 'farzando', 'Yes')} onchange="toggle('div-farzando', true); checkJaaliVisibility()"> Yes</label>
<label><input type="radio" name="farzando" value="No" ${chk('Farzando=<awwala', 'farzando', 'No')} onchange="toggle('div-farzando', false); checkJaaliVisibility()"> No</label>
</div>
</div>
<div id="div-farzando" class="${getVal('Farzando=<awwala', 'farzando') === 'Yes' ? '' : 'hidden'} pref-sub">
<input type="text" name="farzando_details" placeholder="Names(age)..." value="${getVal('Names(age)', 'Names(AgE)', 'names(age)')}">
</div>
</div>
<div class="pref-item" style="flex-direction: column; align-items: flex-start;">
<div style="display: flex; width: 100%; align-items: flex-start;">
<div class="pref-label">Dikrio (=>sania, non-misaq)</div>
<div class="pref-options">
<label><input type="radio" name="dikrio" value="Yes" ${chk('Dikrio=>sania(non-misaq)', 'dikrio', 'Yes')} onchange="toggle('div-dikrio', true)"> Yes</label>
<label><input type="radio" name="dikrio" value="No" ${chk('Dikrio=>sania(non-misaq)', 'dikrio', 'No')} onchange="toggle('div-dikrio', false)"> No</label>
</div>
</div>
<div id="div-dikrio" class="${getVal('Dikrio=>sania(non-misaq)', 'dikrio') === 'Yes' ? '' : 'hidden'} pref-sub">
<input type="text" name="dikrio_details" placeholder="Dikrinames(age)..." value="${getVal('Dikrinames(age)', 'Dikrinames(AgE)', 'dikrinames(age)')}">
</div>
</div>
<div class="pref-item" style="flex-direction: column; align-items: flex-start;">
<div style="display: flex; width: 100%; align-items: flex-start;">
<div class="pref-label">Mehmaan</div>
<div class="pref-options">
<label><input type="radio" name="mehmaan" value="Yes" ${chk('Mehmaan', 'mehmaan', 'Yes')} onchange="toggle('div-mehmaan', true)"> Yes</label>
<label><input type="radio" name="mehmaan" value="No" ${chk('Mehmaan', 'mehmaan', 'No')} onchange="toggle('div-mehmaan', false)"> No</label>
</div>
</div>
<div id="div-mehmaan" class="${getVal('Mehmaan', 'mehmaan') === 'Yes' ? '' : 'hidden'} pref-sub">
<input type="text" name="mehmaan_name" placeholder="MehmaanName" value="${data.MHName || data.mhname || ''}">
<input type="text" name="mehmaan_email" placeholder="MehmaanEmail" value="${data.MHEmail || data.mhemail || ''}">
<input type="text" name="mehmaan_phone" placeholder="MehmaanPhone" value="${data.MHPhone || data.mhphone || ''}">
</div>
</div>
<div class="pref-item hidden" id="div-jaali">
<div class="pref-label">Jaali</div>
<div class="pref-options">
<label><input type="radio" name="jaali" value="Yes" ${chk('Jaali', 'jaali', 'Yes')}> Yes</label>
<label><input type="radio" name="jaali" value="No" ${chk('Jaali', 'jaali', 'No')}> No</label>
</div>
</div>
<div style="text-align: right; margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
<button type="button" class="btn btn-primary" onclick="submitForm()">Submit Preferences</button>
</div>
</form>
`;
container.innerHTML = html;
checkJaaliVisibility();
}
function toggle(id, show) {
const el = document.getElementById(id);
if (el) {
if (show) el.classList.remove('hidden');
else el.classList.add('hidden');
}
}
function checkJaaliVisibility() {
const form = document.getElementById('prefForm');
if (!form) return;
const chrNo = form.querySelector('input[name="chair"][value="No"]');
const frzNo = form.querySelector('input[name="farzando"][value="No"]');
const div = document.getElementById('div-jaali');
if (div) {
if (chrNo && chrNo.checked && frzNo && frzNo.checked) div.classList.remove('hidden');
else div.classList.add('hidden');
}
}
function submitForm() {
const form = document.getElementById('prefForm');
if (!form) return;
const userEmail = currentUser.user.Email || currentUser.user.email;
const formData = {
email: userEmail,
uid: currentUser.user.uid,
att_daily: form.att_daily.checked,
att_wkd: form.att_wkd.checked,
att_lf: form.att_lf.checked,
att_lq: form.att_lq.checked,
chair: form.querySelector('input[name="chair"]:checked') ? form.querySelector('input[name="chair"]:checked').value : '',
farzando: form.querySelector('input[name="farzando"]:checked') ? form.querySelector('input[name="farzando"]:checked').value : '',
farzando_details: form.farzando_details.value,
dikrio: form.querySelector('input[name="dikrio"]:checked') ? form.querySelector('input[name="dikrio"]:checked').value : '',
dikrio_details: form.dikrio_details.value,
mehmaan: form.querySelector('input[name="mehmaan"]:checked') ? form.querySelector('input[name="mehmaan"]:checked').value : '',
mehmaan_name: form.mehmaan_name.value,
mehmaan_email: form.mehmaan_email.value,
mehmaan_phone: form.mehmaan_phone.value,
jaali: form.querySelector('input[name="jaali"]:checked') ? form.querySelector('input[name="jaali"]:checked').value : ''
};
const btn = form.querySelector('button');
if (btn) {
btn.innerText = 'Saving...';
btn.disabled = true;
}
google.script.run
.withSuccessHandler(res => {
currentUser = res;
// After submission, as per convention, we can show the dashboard with updated values
// Or we show Shukran. I'll stick to initDashboard but ensure views switch cleanly.
initDashboard();
})
.withFailureHandler(e => {
if (btn) {
btn.innerText = 'Submit Preferences';
btn.disabled = false;
}
alert("Error: " + e.message);
})
.submitPreferences(formData);
}
</script>
// --- seats.gs ---
// Handles Daily Jagah and Layali Fadela Jagah Logic
/* UPDATE BY ANTIGRAVITY: Phase Logic & Seat Configuration */
/* Shared Label Logic */
function getGlobalSeatLabels() {
return {
'BRHN': 'Burhani',
'MHMD': 'Mohammedi',
'SAF': 'Saifee',
'SEH': 'Sahen',
'MAW': 'Mawaid',
'MAW2': 'MawaidBH'
};
}
function getSeatConfig(type, email) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
// 1. Determine Target Sheet & Phase
const jagahTab = (type === 'Daily') ? TABS.DAILY_JAGAH : TABS.LF_JAGAH;
const jagahSheet = ss.getSheetByName(jagahTab);
if (!jagahSheet) return { success: false, message: "Seat data unavailable." };
const allData = jagahSheet.getDataRange().getDisplayValues();
if (allData.length < 1) return { success: false, message: "No seat configuration found." };
const headers = allData[0]; // Row 1
const seatColumns = [];
// Mapping of codes to labels (Dynamic Legend)
const LABELS = getGlobalSeatLabels();
headers.forEach((h, i) => {
if (i === 0 || i > 14) return; // Skip Date (A) & Limit to O
let label = h;
for (let code in LABELS) {
if (h.includes(code)) { label = LABELS[code]; break; }
}
seatColumns.push({ id: h, label: label, colIndex: i });
});
// Get User Name for "Own Seat" detection
const { rows: dataRows } = fetchTabRows(ss, TABS.DATA);
const dataUser = dataRows.find(r => (r.Email || r.email || '').toLowerCase() === String(email).toLowerCase());
const userName = dataUser ? (dataUser.Name || dataUser.name || '') : '';
const userNorm = kNorm(userName);
let targetRows = [];
if (type === 'Daily') {
const activePhaseRaw = PropertiesService.getScriptProperties().getProperty('ACTIVE_PHASE') || '';
const activePhaseNum = parseInt(activePhaseRaw.replace(/\D/g, '')) || 0;
if (activePhaseNum < 1 || activePhaseNum > 3) {
return { success: false, message: "No active phase deployed." };
}
const pStart = (activePhaseNum === 1) ? 1 : (activePhaseNum === 2) ? 9 : 17;
const pEnd = pStart + 8;
const allRawData = jagahSheet.getDataRange().getValues(); // Get real Dates
for (let r = pStart; r < pEnd; r++) {
if (allData[r] && allData[r][0]) {
targetRows.push({
dateKey: String(allData[r][0]).trim(),
rowIdx: r,
dateObj: allRawData[r][0]
});
}
}
} else {
const allRawData = jagahSheet.getDataRange().getValues();
for (let r = 1; r < allData.length; r++) {
const dVal = String(allData[r][0]).trim();
if (dVal) targetRows.push({
dateKey: dVal,
rowIdx: r,
dateObj: allRawData[r][0]
});
}
}
const grid = targetRows.map(rowInfo => {
const gridRowCells = allData[rowInfo.rowIdx];
const items = [];
let filledCount = 0;
seatColumns.forEach(sc => {
const occupant = String(gridRowCells[sc.colIndex] || '').trim();
const isMe = (occupant !== '' && (occupant === userName || kNorm(occupant) === userNorm));
const isFull = (occupant !== '' && !isMe);
if (isFull) filledCount++;
items.push({
id: sc.id,
label: sc.label,
occupant: isMe ? 'YOU' : (isFull ? 'Full' : ''),
isAvailable: !isFull, // Your own seat is "available" for swap/re-select
isMe: isMe
});
});
const curDate = new Date();
const mySeatObj = items.find(s => s.isMe);
return {
date: rowInfo.dateKey,
label: rowInfo.dateKey,
isFull: (filledCount === seatColumns.length),
isLocked: (rowInfo.dateObj instanceof Date && (rowInfo.dateObj.getTime() - curDate.getTime()) / (1000 * 60 * 60) < 48),
mySeat: mySeatObj ? mySeatObj.id : null,
seats: items
};
});
return { success: true, type: type, seatLabels: LABELS, dates: grid };
}
/** VERSION: V4.2.2 (Emergency Login Fix) **/
function getUserBookings(email) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const { rows: dataRows } = fetchTabRows(ss, TABS.DATA);
const searchEmail = String(email).trim().toLowerCase();
const recordsWithEmail = dataRows.filter(r => (r.Email || r.email || '').toLowerCase() === searchEmail);
if (recordsWithEmail.length === 0) return [];
let myHofId = '';
for (let r of recordsWithEmail) {
const h = String(r.hofid || r.HOFID || '').trim();
if (h && h !== '-' && h !== '0') { myHofId = h; break; }
}
let familyGroup = myHofId ? dataRows.filter(r => String(r.hofid || r.HOFID || '').trim() === myHofId) : recordsWithEmail;
const myNames = familyGroup.map(u => String(u.Name || u.name || u.FullName || u.fullname || '').trim()).filter(n => n !== '');
const myNorms = myNames.map(n => kNorm(n));
const bookings = [];
const scan = (tab, typeTag) => {
const s = ss.getSheetByName(tab);
if (!s) return;
const data = s.getDataRange().getDisplayValues();
const headers = data[0];
for (let r = 1; r < data.length; r++) {
const dateKey = String(data[r][0]).trim();
if (!dateKey) continue;
for (let c = 1; c < data[r].length; c++) {
const val = String(data[r][c]).trim();
if (!val) continue;
const valNorm = kNorm(val);
// Strict match on Normalized name only - avoids partial word matches like "Fatema" vs "Fatema Kapasi"
let isMatch = myNorms.includes(valNorm);
if (isMatch) {
bookings.push({
type: typeTag,
date: dateKey,
label: dateKey,
seat: String(headers[c] || '').trim(),
occupant: val
});
}
}
}
};
scan(TABS.DAILY_JAGAH, 'Daily');
scan(TABS.LF_JAGAH, 'LF');
return bookings;
}
function submitSeatSelection(email, payload) {
const lock = LockService.getScriptLock();
try { lock.waitLock(10000); } catch (e) { return { success: false, message: "System busy." }; }
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
try {
const { rows: capRows, headers: capHeaders } = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
const capRowObj = capRows.find(r => String(r.Email || r.email).toLowerCase() === String(email).toLowerCase());
if (!capRowObj) { lock.releaseLock(); return { success: false, message: "User quota not found." }; }
const capSheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const liveVals = capSheet.getRange(capRowObj._rowNum, 1, 1, capHeaders.length).getValues()[0];
const findI = (ks) => capHeaders.findIndex(h => ks.some(k => String(h).toLowerCase().includes(k.toLowerCase())));
const qIdx = (payload.type === 'Daily') ? findI(['DailyQuota']) : findI(['LFQuota']);
const uIdx = (payload.type === 'Daily') ? findI(['DailyUsed']) : findI(['LFUsed']);
const lIdx = (payload.type === 'Daily') ? findI(['DailyLeft']) : findI(['LFLeft']);
const limit = Number(liveVals[qIdx]);
const used = Number(liveVals[uIdx]);
const { rows: dataRows } = fetchTabRows(ss, TABS.DATA);
const dataUser = dataRows.find(r => (r.Email || r.email || '').toLowerCase() === String(email).toLowerCase());
const userName = dataUser ? (dataUser.Name || dataUser.name || '') : 'User';
const jagahTab = (payload.type === 'Daily') ? TABS.DAILY_JAGAH : TABS.LF_JAGAH;
const gridSheet = ss.getSheetByName(jagahTab);
const gridData = gridSheet.getDataRange().getDisplayValues();
const gridHeaders = gridData[0];
// Pre-validate Quota limits
// Calculate how many *new* slots this request will consume
const newSlotsNeeded = payload.selections.filter(sel => {
const rIdx = gridData.findIndex(row => String(row[0]) === sel.date);
const cIdx = gridHeaders.indexOf(sel.seat);
if (rIdx === -1 || cIdx === -1) return false;
const cellVal = gridData[rIdx][cIdx];
// Only counts as new usage if I don't already own it
const isMine = (cellVal === userName || kNorm(cellVal) === kNorm(userName));
return !isMine;
}).length;
if (used + newSlotsNeeded > limit) {
lock.releaseLock();
return { success: false, message: `Quota exceeded. You have ${limit - used} slots left, but selected ${newSlotsNeeded}.` };
}
const successList = [];
payload.selections.forEach(sel => {
const rIdx = gridData.findIndex(row => String(row[0]) === sel.date);
const cIdx = gridHeaders.indexOf(sel.seat);
if (rIdx > -1 && cIdx > -1) {
const cell = gridSheet.getRange(rIdx + 1, cIdx + 1);
const cur = cell.getValue();
// If it's already mine, skip or overwrite
if (cur === '' || cur === userName || kNorm(cur) === kNorm(userName)) {
if (cur === '') successList.push(sel);
cell.setValue(userName);
}
}
});
if (successList.length > 0) {
const newUsed = used + successList.length;
capSheet.getRange(capRowObj._rowNum, uIdx + 1).setValue(newUsed);
if (lIdx > -1) capSheet.getRange(capRowObj._rowNum, lIdx + 1).setValue(limit - newUsed);
}
lock.releaseLock();
return { success: true, booked: successList.length };
} catch (e) { lock.releaseLock(); return { success: false, message: e.message }; }
}
function cancelBooking(email, type, dateStr, seat) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const jagahTab = (type === 'Daily') ? TABS.DAILY_JAGAH : TABS.LF_JAGAH;
const sheet = ss.getSheetByName(jagahTab);
const data = sheet.getDataRange().getValues();
const displayData = sheet.getDataRange().getDisplayValues();
let rIdx = -1;
const today = new Date();
for (let i = 0; i < data.length; i++) {
if (String(displayData[i][0]).trim() === String(dateStr).trim()) {
rIdx = i;
const dVal = data[i][0];
if (dVal instanceof Date) {
const diffHrs = (dVal.getTime() - today.getTime()) / (1000 * 60 * 60);
if (diffHrs < 48) return { success: false, message: "Action locked within 48h." };
}
break;
}
}
const cIdx = displayData[0].findIndex(h => String(h).trim().toLowerCase() === String(seat).trim().toLowerCase());
const lock = LockService.getScriptLock();
try { lock.waitLock(5000); } catch(e) { return { success: false, message: "System busy (lock timeout)." }; }
Logger.log(`Cancel request: Email=${email}, Type=${type}, Date=${dateStr}, Seat=${seat}`);
Logger.log(`Found rIdx=${rIdx}, cIdx=${cIdx}`);
if (rIdx === -1) { lock.releaseLock(); return { success: false, message: `Row for date '${dateStr}' not found.` }; }
if (cIdx === -1) { lock.releaseLock(); return { success: false, message: `Seat header '${seat}' not found.` }; }
const cell = sheet.getRange(rIdx + 1, cIdx + 1);
const currentValue = cell.getValue();
Logger.log(`Current cell value: '${currentValue}'`);
cell.setValue("");
SpreadsheetApp.flush(); // Force immediate write
Logger.log(`Cell cleared successfully`);
const { rows: capRows, headers: capHeaders } = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
const capRowObj = capRows.find(r => String(r.Email || r.email || '').toLowerCase() === String(email).toLowerCase());
if (capRowObj) {
const capSheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const findI = (ks) => capHeaders.findIndex(h => {
if (!h) return false;
const hStr = String(h).toLowerCase();
return ks.some(k => hStr.includes(k.toLowerCase()));
});
const uIdx = (type === 'Daily') ? findI(['DailyUsed']) : findI(['LFUsed']);
const qIdx = (type === 'Daily') ? findI(['DailyQuota']) : findI(['LFQuota']);
const lIdx = (type === 'Daily') ? findI(['DailyLeft']) : findI(['LFLeft']);
const curU = Number(capSheet.getRange(capRowObj._rowNum, uIdx + 1).getValue()) || 0;
const curQ = Number(capSheet.getRange(capRowObj._rowNum, qIdx + 1).getValue()) || 0;
const nextU = Math.max(0, curU - 1);
capSheet.getRange(capRowObj._rowNum, uIdx + 1).setValue(nextU);
if (lIdx > -1) capSheet.getRange(capRowObj._rowNum, lIdx + 1).setValue(curQ - nextU);
}
lock.releaseLock();
return { success: true };
}
function getUserQuotas(email) {
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const { rows } = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
const row = rows.find(r => String(r.Email || r.email || '').toLowerCase() === String(email).toLowerCase());
if (!row) return null;
const dQ = Number(row.dailyquota || 0);
const dU = Number(row.dailyused || 0);
const lQ = Number(row.lfquota || 0);
const lU = Number(row.lfused || 0);
return {
daily: { limit: dQ, used: dU, left: Math.max(0, dQ - dU) },
lf: { limit: lQ, used: lU, left: Math.max(0, lQ - lU) }
};
}
function getFamilySeatStatus(email) {
const bookings = getUserBookings(email);
return { hasSelectedDaily: bookings.some(b => b.type === 'Daily'), hasSelectedLF: bookings.some(b => b.type === 'LF'), bookingCount: bookings.length };
}
function cancelBookingsBatch(email, type, cancellations) {
const lock = LockService.getScriptLock();
try { lock.waitLock(10000); } catch (e) { return { success: false, message: "System busy." }; }
const ss = SpreadsheetApp.openById(DATA_SHEET_ID);
const jagahTab = (type === 'Daily') ? TABS.DAILY_JAGAH : TABS.LF_JAGAH;
const sheet = ss.getSheetByName(jagahTab);
const data = sheet.getDataRange().getValues();
const displayData = sheet.getDataRange().getDisplayValues(); // For string matching
let successCount = 0;
const today = new Date();
// 1. Process deletions
// We can optimize by mapping dates to row indices first
const dateRowMap = {};
for (let i = 0; i < displayData.length; i++) {
const dKey = String(displayData[i][0]).trim();
if (dKey) dateRowMap[dKey] = { index: i, dateObj: data[i][0] };
}
// Headers for column lookup
const headers = displayData[0].map(h => String(h).trim().toLowerCase());
cancellations.forEach(item => {
const rowInfo = dateRowMap[String(item.date).trim()];
if (!rowInfo) return; // Date not found
// 48h Lock check
if (rowInfo.dateObj instanceof Date) {
const diffHrs = (rowInfo.dateObj.getTime() - today.getTime()) / (1000 * 60 * 60);
if (diffHrs < 48) return; // Locked
}
const cIdx = headers.indexOf(String(item.seat).trim().toLowerCase());
if (cIdx === -1) return; // Seat not found
sheet.getRange(rowInfo.index + 1, cIdx + 1).setValue("");
successCount++;
});
if (successCount > 0) {
SpreadsheetApp.flush();
// 2. Update Quota
const { rows: capRows, headers: capHeaders } = fetchTabRows(ss, TABS.AVAILABLE_CAPACITY);
const capRowObj = capRows.find(r => String(r.Email || r.email || '').toLowerCase() === String(email).toLowerCase());
if (capRowObj) {
const capSheet = ss.getSheetByName(TABS.AVAILABLE_CAPACITY);
const findI = (ks) => capHeaders.findIndex(h => {
if (!h) return false;
const hStr = String(h).toLowerCase();
return ks.some(k => hStr.includes(k.toLowerCase()));
});
const uIdx = (type === 'Daily') ? findI(['DailyUsed']) : findI(['LFUsed']);
const qIdx = (type === 'Daily') ? findI(['DailyQuota']) : findI(['LFQuota']);
const lIdx = (type === 'Daily') ? findI(['DailyLeft']) : findI(['LFLeft']);
const curU = Number(capSheet.getRange(capRowObj._rowNum, uIdx + 1).getValue()) || 0;
const curQ = Number(capSheet.getRange(capRowObj._rowNum, qIdx + 1).getValue()) || 0;
const nextU = Math.max(0, curU - successCount);
capSheet.getRange(capRowObj._rowNum, uIdx + 1).setValue(nextU);
if (lIdx > -1) capSheet.getRange(capRowObj._rowNum, lIdx + 1).setValue(curQ - nextU);
}
}
lock.releaseLock();
return { success: true, count: successCount };
}
<!-- VERSION: V4.3.0 (Compact & Robust) -->
<!-- ADDED BY ANTIGRAVITY: Modular Selection System (Inline) -->
<div id="seat-selection-view" class="hidden"
style="margin:15px 0; border:1px solid var(--color-accent); background:#fff; padding:15px; border-radius:12px; box-shadow:0 2px 8px rgba(0,0,0,0.05);">
<div class="header"
style="border-bottom:1px solid #eee; margin-bottom:15px; padding-bottom:10px; display:flex; justify-content:space-between; align-items:center;">
<h2 id="seatSelectionTitle" style="margin:0; font-size:1.1rem; color:var(--color-primary);">Seat Selection</h2>
<button class="btn" style="background:transparent; border:none; color:#999; font-size:1.2rem; padding:0 10px;"
onclick="closeSeatSelection()">×</button>
</div>
<div>
<div id="selection-quota-header" style="margin-bottom:15px;"></div>
<div id="seat-selection-inner-content">
<!-- Populated via renderSeatGridModular -->
</div>
</div>
</div>
<script>
function loadSeatSelectionModular(type) {
console.log("Loading modular selection for:", type);
const email = currentUser.user.Email || currentUser.user.email;
const inner = document.getElementById('seat-selection-inner-content');
const box = document.getElementById('seat-selection-view');
const anchorId = (type === 'LF') ? 'selection-lf-anchor' : 'selection-daily-anchor';
const otherAnchorId = (type === 'LF') ? 'selection-daily-anchor' : 'selection-lf-anchor';
const anchor = document.getElementById(anchorId);
const otherAnchor = document.getElementById(otherAnchorId);
if (anchor) {
anchor.appendChild(box);
box.classList.remove('hidden');
}
if (otherAnchor) otherAnchor.innerHTML = '';
inner.innerHTML = '<div class="loader" style="display:block;"></div>';
document.getElementById('seatSelectionTitle').innerText = `${type} Jagah Selection`;
google.script.run
.withSuccessHandler(res => {
try {
console.log("Seat Config Res:", res);
if (!res || !res.success) {
showModal("Notice", (res && res.message) ? res.message : "No seats available.", "info");
closeSeatSelection();
return;
}
renderSeatGridModular(res, type);
} catch (err) {
console.error("Render Error:", err);
showModal("Render Error", "Client failed to render seat grid: " + err.message, "error");
// closeSeatSelection(); // Keep open to see error?
}
})
.withFailureHandler(e => {
showModal("System Error", e.message, "error");
closeSeatSelection();
})
.getSeatConfig(type, email);
}
function renderSeatGridModular(res, type) {
const inner = document.getElementById('seat-selection-inner-content');
let html = `
<div class="table-container" style="max-height:60vh; overflow-y:auto; overflow-x:auto;">
<table style="width:100%; min-width:100%; border-collapse:collapse; background:#fff; font-size:0.85rem; border:1px solid #eee;">
<thead style="background:#f9f9f9; position:sticky; top:0; z-index:20;">
<tr>
<th style="text-align:left; padding:8px; border-bottom:2px solid #eee; position:sticky; left:0; background:#f9f9f9; z-index:21; border-right:2px solid #eee;">Date</th>
<th style="text-align:left; padding:8px; border-bottom:2px solid #eee;">Selection</th>
</tr>
</thead>
<tbody>
`;
res.dates.forEach(row => {
const isRowLocked = row.isLocked;
const rowStyle = row.isFull ? 'style="background-color:#9CF4DF;"' : '';
// Fix: Explicitly calculate variables
const mySeat = row.seats.find(s => s.isMe);
const hadSeat = !!mySeat;
// Fix: Generate cell content explicitly
// LOGIC CHANGE: 48h Lock only prevents CANCELLATION/CHANGE of existing seat.
// It does NOT prevent NEW selection.
let cellContent = '';
if (isRowLocked && hadSeat) {
// User HAS a seat, but it's locked (cannot cancel/change)
cellContent = `<span style="color:#D32F2F; font-size:0.8rem; font-weight:600; display:block;">Locked<br><span style="font-weight:400; color:#666;">(Time limit)</span></span>
<input type="hidden" id="mod-sel-${row.date}" value="${mySeat.id}" data-had-seat="true" disabled>`;
// Hidden input ensuring we track it but can't change it?
// Actually, if we just don't render the select, the 'submit' logic needs to handle missing inputs.
// Submit logic checks 'querySelectorAll' - so if it's not a select, it won't be picked up.
// That's fine, "Locked" means no change submitted for this date.
} else if (row.isFull && !hadSeat) {
// Full and I don't have a seat
cellContent = '<span style="color:#727AC0; font-weight:bold;">Full</span>';
} else {
// Available OR Locked-but-I-dont-have-a-seat-yet (So I can book)
const optionsHtml = row.seats.filter(s => s.isAvailable).map(s => {
const isCurrent = s.isMe;
return `<option value="${s.id}" data-is-me="${isCurrent}" ${isCurrent ? 'selected' : ''}>${s.id}</option>`;
}).join('');
cellContent = `
<select id="mod-sel-${row.date}" data-had-seat="${hadSeat}" onchange="updateLiveQuota('${type}')" style="margin:0; flex:1; font-size:0.85rem; min-width:140px;">
<option value="" data-is-me="false">-- Select --</option>
${optionsHtml}
</select>
`;
}
html += `
<tr ${rowStyle}>
<td style="padding:8px; border-bottom:1px solid #eee; position:sticky; left:0; background:${row.isFull ? '#9CF4DF' : '#fff'}; z-index:10; border-right:2px solid #eee;">
<b>${row.label}</b>
<div style="font-size:0.75rem; color:#888; white-space:nowrap;">${row.date}</div>
</td>
<td style="padding:8px; border-bottom:1px solid #eee;">
${cellContent}
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<div style="display:flex; gap:10px; justify-content:center; margin-top:20px; border-top:1px solid #eee; padding-top:15px;">
<button class="btn" style="min-width:120px; background:transparent; border:1px solid #ccc; font-size:0.9rem;" onclick="closeSeatSelection()">Cancel</button>
<button id="btn-mod-submit" class="btn btn-primary" style="min-width:120px; font-size:0.9rem;" onclick="submitModularSelection('${type}')">Confirm</button>
</div>
`;
inner.innerHTML = html;
updateLiveQuota(type);
}
function updateLiveQuota(type) {
const typeKey = type.toLowerCase();
const quotaInfo = currentUser.userQuotas[typeKey];
// Base numbers from backend (snapshot at load)
const limit = quotaInfo ? parseInt(quotaInfo.limit || 0) : 0;
const initialUsed = quotaInfo ? parseInt(quotaInfo.used || 0) : 0;
let delta = 0;
let newSelectionsCount = 0;
document.querySelectorAll('select[id^="mod-sel-"]').forEach(sel => {
const hadSeat = (sel.getAttribute('data-had-seat') === 'true');
const hasSelectionNow = !!sel.value;
if (hasSelectionNow) {
newSelectionsCount++; // Just for visual "X selected"
}
// Logic:
// 1. Had Seat -> No Selection Now = Freed (-1)
// 2. No Seat -> Has Selection Now = Consumed (+1)
// 3. Had Seat -> Has Selection Now = Swap (0)
// 4. No Seat -> No Selection Now = No Change (0)
if (hadSeat && !hasSelectionNow) {
delta--;
} else if (!hadSeat && hasSelectionNow) {
delta++;
}
});
const projectedUsed = initialUsed + delta;
const projectedLeft = Math.max(0, limit - projectedUsed);
const isOver = (projectedUsed > limit);
const btn = document.getElementById('btn-mod-submit');
const modalBox = document.getElementById('selection-quota-header');
if (btn) {
btn.disabled = isOver;
btn.style.opacity = isOver ? '0.5' : '1';
btn.style.cursor = isOver ? 'not-allowed' : 'pointer';
btn.innerText = isOver ? `Quota Exceeded (${projectedUsed}/${limit})` : "Confirm";
}
if (modalBox) {
modalBox.style.borderRadius = "30px";
modalBox.style.padding = "8px 15px";
modalBox.style.background = isOver ? "#fff0f0" : "#f0f7ff";
modalBox.style.border = isOver ? "1px solid #ffcccc" : "1px solid #d0e1f9";
let msgColor = isOver ? "#e67e22" : "var(--color-primary)";
modalBox.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; font-size:0.85rem;">
<span style="font-weight:600; color:var(--color-primary);">Monthly Limit: ${projectedUsed} / ${limit}</span>
<span style="color:#555;">Selected: <b style="color:${msgColor};">${newSelectionsCount}</b></span>
</div>
`;
}
}
function submitModularSelection(type) {
const btn = document.getElementById('btn-mod-submit');
const email = currentUser.user.Email || currentUser.user.email;
const selections = [];
document.querySelectorAll('select[id^="mod-sel-"]').forEach(sel => {
if (sel.value) {
selections.push({
date: sel.id.replace('mod-sel-', ''),
seat: sel.value
});
}
});
if (selections.length === 0) {
showModal("Selection Required", "Please select at least one seat.", "info");
return;
}
btn.innerText = "Processing...";
btn.disabled = true;
google.script.run
.withSuccessHandler(res => {
if (res.success) {
showModal("Success!", `Successfully updated selections.`, "success");
closeSeatSelection();
doLogin();
} else {
showModal("Error", res.message, "error");
btn.innerText = "Confirm";
btn.disabled = false;
}
})
.withFailureHandler(e => {
showModal("System Error", e.message, "error");
btn.innerText = "Confirm";
btn.disabled = false;
})
.submitSeatSelection(email, { type: type, selections: selections });
}
function closeSeatSelection() {
window.activeModularType = null;
document.getElementById('seat-selection-view').classList.add('hidden');
}
</script>
<style>
:root {
--color-primary: #594346;
--color-secondary: #e9e2e5;
--color-bg: #F9F5F7;
--color-accent: #29ABB1;
--color-highlight: #9CF4DF;
--color-button-bg: #F7EBF2;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', sans-serif;
background: var(--color-bg);
margin: 0;
}
/* User / Login Layout */
body.user-layout {
padding: 15px;
color: var(--color-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
/* Admin Layout */
body.admin-layout {
display: flex;
height: 100vh;
overflow: hidden;
padding: 0;
}
.container {
width: 100%;
max-width: 450px;
background: #fff;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
margin-top: 15px;
}
.container-wide {
max-width: 900px;
}
.hidden {
display: none !important;
}
.btn {
background: var(--color-button-bg);
color: var(--color-primary);
border: none;
padding: 8px 18px;
border-radius: 24px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
margin: 3px;
transition: all 0.2s;
}
.btn:hover {
background: #e5d6df;
transform: translateY(-1px);
}
.btn-primary {
background: var(--color-primary);
color: #fff;
}
input:not([type="checkbox"]):not([type="radio"]),
select {
width: 100%;
padding: 8px;
margin: 3px 0;
border: 2px solid #eee;
border-radius: 8px;
font-family: inherit;
font-size: 0.9rem;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--color-primary);
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 15px auto;
display: none;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #eee;
padding-bottom: 12px;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
}
.pref-section {
background: #fafafa;
padding: 15px;
border-radius: 12px;
margin-bottom: 15px;
}
.pref-section h3 {
margin-bottom: 12px;
font-size: 1.1rem;
}
.pref-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
line-height: 1.6;
}
.pref-label {
min-width: 200px;
font-weight: 600;
font-size: 0.95rem;
padding-right: 10px;
}
.pref-options {
display: flex;
gap: 15px;
align-items: flex-start;
}
.pref-options.vertical {
flex-direction: column;
gap: 8px;
}
.pref-options label {
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
white-space: nowrap;
}
.pref-options input[type="checkbox"],
.pref-options input[type="radio"] {
width: 18px !important;
height: 18px !important;
margin: 0 6px 0 0 !important;
}
.pref-sub {
margin-left: 210px;
margin-top: 5px;
width: calc(100% - 210px);
}
.family-card {
background: #fff;
padding: 12px;
border-radius: 8px;
border-left: 4px solid var(--color-accent);
margin-bottom: 10px;
font-size: 0.9rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.family-card .name {
font-weight: 600;
font-size: 1rem;
margin-bottom: 4px;
}
.family-card .prefs {
color: #555;
line-height: 1.4;
margin-top: 6px;
padding-top: 6px;
border-top: 1px dotted #eee;
}
@media (max-width: 600px) {
body {
padding: 10px;
}
.container {
padding: 15px;
max-width: 100%;
}
.pref-item {
flex-direction: column;
}
.pref-label {
min-width: 100%;
margin-bottom: 8px;
}
.pref-options {
gap: 10px;
flex-direction: column;
align-items: flex-start;
}
.pref-options label {
white-space: normal;
}
.pref-sub {
margin-left: 0;
width: 100%;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.header>div {
width: 100%;
}
.header button {
width: 100%;
margin-bottom: 5px;
}
/* Make tables scrollable on mobile */
table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* Stack admin choice buttons vertically on mobile */
#admin-choice {
flex-direction: column !important;
}
#admin-choice button {
width: 100% !important;
}
/* Make emergency back button smaller on mobile */
#emergency-back-btn {
bottom: 10px;
right: 10px;
}
#emergency-back-btn button {
padding: 8px 12px;
font-size: 0.8rem;
}
/* Make quota stats boxes stack on mobile */
#dash-stats-daily,
#dash-stats-lf {
flex-direction: column;
gap: 8px;
padding: 10px;
}
#dash-stats-daily>div,
#dash-stats-lf>div {
border-right: none !important;
padding-right: 0 !important;
border-bottom: 1px solid #ddd;
padding-bottom: 8px;
}
#dash-stats-daily>div:last-child,
#dash-stats-lf>div:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
</style>
<style>
/* =========================================
ADMIN DASHBOARD STYLES
========================================= */
/* Left Navigation */
.nav-panel {
width: 200px;
background: var(--color-primary);
color: #fff;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.brand {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 30px;
}
.nav-item {
padding: 12px 10px;
cursor: pointer;
margin-bottom: 5px;
transition: all 0.2s;
font-size: 1rem;
color: #ecf0f1;
display: flex;
align-items: center;
gap: 10px;
}
.nav-item:hover,
.nav-item.active {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #fff;
}
/* Right Content */
.content {
flex: 1;
padding: 25px;
overflow-y: auto;
background: var(--color-bg);
}
.section {
display: none;
}
.section.active {
display: block;
}
h1 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
h2 {
color: var(--color-primary);
margin: 20px 0 10px 0;
font-size: 1.3rem;
}
/* Stats */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.stat-label {
font-size: 0.9rem;
color: #666;
margin-bottom: 8px;
}
.stat-value {
font-size: 2rem;
font-weight: 600;
color: var(--color-primary);
}
/* Tables */
.table-container {
background: #fff;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
overflow-x: auto;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
gap: 10px;
}
.search-bar {
padding: 8px 12px;
border: 2px solid #eee;
border-radius: 8px;
width: 300px;
font-family: inherit;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 1000px;
}
th {
background: var(--color-accent);
color: #fff;
padding: 10px 8px;
text-align: left;
position: sticky;
top: 0;
font-size: 0.85rem;
border: 1px solid #fff;
}
td {
padding: 8px;
border: 1px solid #ddd;
font-size: 0.8rem;
}
tr:hover {
background: #f9f9f9;
/* Fix for blue background on hover overriding selection */
}
input.edit-cell {
width: 45px !important;
padding: 4px !important;
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.8rem;
}
input[type="checkbox"] {
width: auto;
cursor: pointer;
}
.pagination {
display: flex;
gap: 5px;
justify-content: center;
margin-top: 15px;
}
.page-btn {
padding: 5px 10px;
border: 1px solid var(--color-primary);
background: #fff;
cursor: pointer;
border-radius: 4px;
font-size: 0.85rem;
}
.page-btn.active {
background: var(--color-primary);
color: #fff;
}
/* Responsive Admin */
@media (max-width: 768px) {
.nav-panel {
width: 150px;
padding: 15px;
}
.content {
padding: 15px;
}
.search-bar {
width: 200px;
}
}
/* =========================================
MODAL STYLES (Consolidated)
========================================= */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
/* Flex display override when active */
.modal-overlay.flex {
display: flex;
}
.modal-box {
background: #fff;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 450px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
text-align: center;
animation: modalPop 0.3s ease-out;
}
@keyframes modalPop {
0% {
transform: scale(0.9);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal-title {
font-size: 1.5rem;
font-weight: 600;
color: #727AC0;
margin-bottom: 15px;
}
.modal-message {
font-size: 1rem;
color: #444;
line-height: 1.5;
margin-bottom: 25px;
}
.modal-btn {
background: #727AC0;
color: #fff;
border: none;
padding: 10px 30px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.modal-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.modal-error .modal-title {
color: #E2727C;
}
.modal-success .modal-title {
color: #29ABB1;
}
/* Status Text Formatting */
.status-pending {
font-size: 0.85rem;
color: #e67e22;
font-style: italic;
margin-top: 2px;
}
.status-success {
font-size: 0.85rem;
color: green;
font-weight: bold;
margin-top: 2px;
}
/* --- ADMIN DASHBOARD RESPONSIVE STYLES --- */
/* --- GLOBAL TABLE BORDER FIX --- */
table,
th,
td,
.admin-layout table,
.admin-layout th,
.admin-layout td,
#seat-selection-view table,
#seat-selection-view th,
#seat-selection-view td {
border-color: #1f2a44 !important;
}
/* --- GLOBAL TABLE BORDER FIX --- */
table,
th,
td {
border: 1px solid #1f2a44 !important;
}
/* --- GLOBAL TABLE HEADER FIX --- */
/* Ensure all table headers, especially first columns, are readable */
th,
.admin-layout th,
#seat-selection-view th,
#table-daily-container th,
#table-lf-container th,
#daily-jagah-content th,
#lf-jagah-content th,
#behno-table th {
background-color: var(--color-accent) !important;
color: #fff !important;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px !important;
}
/* Ensure sticky columns match */
th:first-child,
td:first-child {
/* ensure proper background for readability if sticky */
}
/* COMPACT TABLE STYLES (Desktop) */
.admin-layout th,
.admin-layout td {
padding: 3px 4px !important;
font-size: 0.85rem !important;
line-height: 1.1 !important;
text-align: left !important;
text-align: left !important;
text-align: left !important;
white-space: normal !important;
/* Allow wrapping */
word-wrap: break-word;
max-width: 150px;
/* Force wrapping for long names */
}
/* Specific narrow width for Attnd column to force wrapping */
td[data-label="Attnd"] {
max-width: 100px;
}
/* Fix Jagah Table Widths & Scrolling */
.admin-layout .table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin-bottom: 20px;
border: 1px solid #eee;
}
/* Strict wrapping for Daily/LF Jagah Names */
#daily-jagah td,
#lf-jagah td {
white-space: normal !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 85px !important;
min-width: 60px !important;
vertical-align: top;
}
#daily-jagah table,
#lf-jagah table {
width: max-content;
/* Allow table to spill over */
min-width: auto;
border-collapse: collapse;
}
/* Sticky First Column (Date) for Desktop */
.admin-layout th:first-child,
.admin-layout td:first-child {
position: sticky;
left: 0;
background-color: #fff;
/* Opaque background over scrolling content */
z-index: 10;
border-right: 2px solid #eee;
}
.admin-layout th:first-child {
z-index: 11;
/* Higher than td */
background-color: var(--color-accent);
/* Match header color so text is visible */
}
/* MOBILE CARD VIEW - NO SCROLLING */
@media (max-width: 768px) {
.admin-layout .table-container {
overflow-x: hidden !important;
}
.admin-layout table,
.admin-layout thead,
.admin-layout tbody,
.admin-layout th,
.admin-layout td,
.admin-layout tr {
display: block;
}
.admin-layout thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
.admin-layout tr {
border: 1px solid #eee;
margin-bottom: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.admin-layout td {
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50% !important;
text-align: right !important;
white-space: normal !important;
word-break: break-word;
}
.admin-layout td:before {
position: absolute;
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
color: #554346;
content: attr(data-label);
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment