Setup Users & Their Availability

Manage Users

Existing Users:

    Check Team Availability

    Select Users to Check:

    Check Results:

      No users configured in Setup tab.

      ' : ''; sac_users.forEach(user => { const label = document.createElement('label'); label.innerHTML = ` ${user.name}`; container.appendChild(label); }); } function sac_performAvailabilityCheck() { const checkerTZ = document.getElementById('sac-checkerTimeZone').value; const selectedUserIds = Array.from(document.querySelectorAll('input[name="sac-checkUser"]:checked')).map(cb => cb.value); const checkDateStr = document.getElementById('sac-checkDate').value; const checkTimeStr = document.getElementById('sac-checkTime').value; const checkDurationMins = parseInt(document.getElementById('sac-checkDuration').value) || 0; if (!checkerTZ || selectedUserIds.length === 0 || !checkDateStr || !checkTimeStr) { alert("Please set your time zone, select user(s), and specify date/time to check."); return; } try { new Intl.DateTimeFormat('en', {timeZone: checkerTZ}); } catch (e) { alert(`Invalid 'Your Time Zone': ${checkerTZ}`); return; } const resultsListEl = document.getElementById('sac-availabilityResultsList'); resultsListEl.innerHTML = ''; // This is the absolute point in time the checker is proposing, from their perspective. // We need to find what this instant corresponds to in each user's local time. // Constructing the date in checker's TZ. The JS Date object stores time as UTC milliseconds internally. const proposedDateTimeChecker = new Date(`${checkDateStr}T${checkTimeStr}:00`); // This gets parsed in browser's local TZ if no TZ specified. This is a common pitfall. // To make it correct, we must interpret checkDateStr+checkTimeStr AS IF they are in checkerTZ. // A robust way: format a reference date (like now) in checkerTZ to find its offset, then apply. // OR, use Intl.DateTimeFormat to get parts in checkerTZ, then construct Date from those parts assuming UTC then adjust. // For simplification for THIS tool using native only, when displaying, we will format from this JS Date instant. // The user must be aware that the input date/time uses standard JS Date parsing behavior (usually browser local). // The checkerTZ input is primarily for display conversion and context. // A more direct way to represent checker's proposed time as a specific UTC instant: // Create a string that represents this time in the checker's TZ, then use it. // This still relies on JS Date parsing. // For accurate conversion, we typically get the date/time parts and the checker's TZ, // then for each user, ask "what is date D, time T in checkerTZ, when viewed from userTZ?" selectedUserIds.forEach(userId => { const user = sac_users.find(u => u.id === userId); if (!user) return; const li = document.createElement('li'); let overallStatus = "Available"; // Assume available initially for the entire duration let reason = ""; const checkStartTimeUserLocalParts = sac_convertInstantToUserLocalParts(proposedDateTimeChecker, user.timeZone); let proposedEndTimeChecker = new Date(proposedDateTimeChecker.getTime() + checkDurationMins * 60000); const checkEndTimeUserLocalParts = sac_convertInstantToUserLocalParts(proposedEndTimeChecker, user.timeZone); // Check status for the interval if (checkDurationMins > 0) { // Simplified: check start, middle, and end of the duration const pointsToCheck = [proposedDateTimeChecker]; if (checkDurationMins > 30) { // Add a middle point for longer durations pointsToCheck.push(new Date(proposedDateTimeChecker.getTime() + (checkDurationMins / 2) * 60000)); } pointsToCheck.push(proposedEndTimeChecker); // Check the very end of the slot let allPointsAvailable = true; for (const point of pointsToCheck) { const { status: pointStatus, reason: pointReason } = sac_checkSinglePoint(user, point); if (pointStatus !== "Available") { allPointsAvailable = false; reason = pointReason; // Take first reason of unavailability break; } } overallStatus = allPointsAvailable ? "Available" : "Unavailable"; // Simplified from partial // More accurate duration check would iterate through slots or find continuous free block if (allPointsAvailable) { // Re-check more thoroughly if all points look good. // This requires checking every slot within the duration, which is more complex. // For this version, if key points are good, we'll assume available. } } else { // Point in time check const singlePointResult = sac_checkSinglePoint(user, proposedDateTimeChecker); overallStatus = singlePointResult.status; reason = singlePointResult.reason; } const userLocalTimeDisplayStart = `${checkStartTimeUserLocalParts.dateStr} ${checkStartTimeUserLocalParts.timeStr}`; const userLocalTimeDisplayEnd = checkDurationMins > 0 ? ` to ${checkEndTimeUserLocalParts.dateStr} ${checkEndTimeUserLocalParts.timeStr}` : ''; li.classList.add(`sac-status-${overallStatus.replace(' ', '-')}`); li.innerHTML = `${user.name}: ${overallStatus} (Proposed: ${userLocalTimeDisplayStart}${userLocalTimeDisplayEnd} in ${user.timeZone}) ${reason ? `
      Reason: ${reason}` : ''}`; resultsListEl.appendChild(li); }); } function sac_convertInstantToUserLocalParts(instant, userTimeZone) { try { const formatter = new Intl.DateTimeFormat('en-CA', { // YYYY-MM-DD timeZone: userTimeZone, year: 'numeric', month: '2-digit', day: '2-digit' }); const timeFormatter = new Intl.DateTimeFormat('en-GB', { // HH:MM timeZone: userTimeZone, hour: '2-digit', minute: '2-digit', hour12: false }); return { dateStr: formatter.format(instant), timeStr: timeFormatter.format(instant), dayKey: sac_daysOfWeekKeys[instant.toLocaleDateString('en-US', {timeZone: userTimeZone, weekday: 'long'}).toLowerCase().substring(0,3) === 'sun' ? 6 : (new Date(instant.toLocaleString('en-US', {timeZone: userTimeZone})).getDay() -1) ] // This is complex }; } catch (e) { console.error(`Error in sac_convertInstantToUserLocalParts for TZ ${userTimeZone}:`, e); return { dateStr: "Err", timeStr: "Err", dayKey: "Err"}; } } function sac_checkSinglePoint(user, dateTimeInstant) { const userLocalParts = sac_convertInstantToUserLocalParts(dateTimeInstant, user.timeZone); if(userLocalParts.dateStr === "Err") return { status: "Unavailable", reason: "Timezone conversion error for user."}; const userDateStr = userLocalParts.dateStr; // YYYY-MM-DD const userTimeStr = userLocalParts.timeStr; // HH:MM // 1. Check specific unavailability events const specificBlock = sac_specificUnavailability.find(event => event.userId === user.id && event.date === userDateStr && userTimeStr >= event.startTime && userTimeStr < event.endTime ); if (specificBlock) { return { status: "Unavailable", reason: specificBlock.reason }; } // 2. Check standard working hours // Get day of week for userDateStr in user's timezone // This is tricky because userDateStr IS ALREADY IN user's timezone. // So new Date(userDateStr) will be interpreted by browser in its *own* TZ. // We need day of week for userDateStr as per user's calendar. const tempDateForDay = new Date(userDateStr + "T12:00:00"); // Midday to avoid DST crossover issues for *just getting day* const dayOfWeekJs = tempDateForDay.getDay(); // 0 for Sun, 1 for Mon... const dayKey = sac_daysOfWeekKeys[dayOfWeekJs === 0 ? 6 : dayOfWeekJs - 1]; // Our Mon-Sun keys const stdHours = sac_standardAvailability[user.id] ? sac_standardAvailability[user.id][dayKey] : null; if (stdHours) { if (stdHours.notWorking || !stdHours.start || !stdHours.end) { return { status: "Unavailable", reason: "Outside working hours (Day off)" }; } if (userTimeStr < stdHours.start || userTimeStr >= stdHours.end) { return { status: "Unavailable", reason: "Outside standard working hours" }; } } else { // No standard hours defined for this day, assume unavailable unless it's a general "always available" policy return { status: "Unavailable", reason: "Working hours not defined for this day" }; } return { status: "Available", reason: "" }; } // --- PDF Download --- document.getElementById('sac-downloadPdfBtn').onclick = async () => { const checkerTZ = document.getElementById('sac-checkerTimeZone').value; const checkDateStr = document.getElementById('sac-checkDate').value; const checkTimeStr = document.getElementById('sac-checkTime').value; const checkDurationMins = parseInt(document.getElementById('sac-checkDuration').value) || 0; const resultsContent = Array.from(document.querySelectorAll('#sac-availabilityResultsList li')); if (!checkerTZ || !checkDateStr || !checkTimeStr || resultsContent.length === 0 || resultsContent[0].textContent.includes("Perform a check")) { alert("Please perform an availability check first before downloading."); return; } const { jsPDF } = window.jspdf; const pdf = new jsPDF('p', 'mm', 'a4'); let currentY = 15; const margin = 15; const pageWidth = pdf.internal.pageSize.getWidth(); pdf.setFontSize(18); pdf.setTextColor('#007BFF'); pdf.text("Team Availability Check Report", pageWidth / 2, currentY, { align: 'center' }); currentY += 8; pdf.setFontSize(10); pdf.setTextColor('#333'); pdf.text(`Check Performed: ${new Date().toLocaleString()}`, pageWidth / 2, currentY, { align: 'center' }); currentY += 8; pdf.text(`Proposed Time (in ${checkerTZ}): ${new Date(checkDateStr+"T"+checkTimeStr).toLocaleString(undefined, {dateStyle:'medium', timeStyle:'short'})} ${checkDurationMins > 0 ? `for ${checkDurationMins} min` : ''}`, margin, currentY); currentY += 10; pdf.setFontSize(12); pdf.setTextColor('#007BFF'); pdf.text("Results:", margin, currentY); currentY += 6; const tableBody = resultsContent.map(li => { const parts = li.textContent.split(':'); const userName = parts[0].trim(); const statusAndDetails = parts.slice(1).join(':').trim(); const statusMatch = statusAndDetails.match(/^(Available|Unavailable|Partially Available)/); const status = statusMatch ? statusMatch[0] : 'Unknown'; const details = statusAndDetails.replace(status, '').trim(); return [userName, status, details]; }); pdf.autoTable({ head: [["User", "Status", "Details (Proposed time in user's local TZ & Reason)"]], body: tableBody, startY: currentY, theme: 'grid', headStyles: { fillColor: '#007BFF', textColor: '#FFFFFF' }, styles: { fontSize: 9, cellPadding: 2 }, columnStyles: { 0: { cellWidth: 40 }, 1: { cellWidth: 30 }, 2: { cellWidth: 'auto'} }, didParseCell: function (data) { if (data.section === 'body') { if (data.cell.raw === 'Available') data.cell.styles.textColor = '#2ECC71'; if (data.cell.raw === 'Unavailable') data.cell.styles.textColor = '#E74C3C'; if (data.cell.raw === 'Partially Available') data.cell.styles.textColor = '#F39C12'; } } }); pdf.save(`Availability_Check_Report_${checkDateStr}.pdf`); }; // --- Initial Load --- document.addEventListener('DOMContentLoaded', () => { sac_openTab(null, sac_tabs[0]); const firstTabButton = document.querySelector(`.sac-tab-button[onclick*="${sac_tabs[0]}"]`); if (firstTabButton) firstTabButton.classList.add('active'); sac_updateNavButtons(); });
      Scroll to Top