v2.0 • All data stored locally

Quick Actions

Team Roster

Manage employees, roles, and traits

📅 Employee Availability

Click cells to cycle: Available (Y)Preferred (P)Unavailable (N)

Preferred shifts get priority in auto-scheduling.

🤝 Team Compatibility

Rate how well each pair works together. The scheduler avoids putting conflicting pairs on the same shift.

Conflict = avoid together Friction = try to avoid Good = works well Great = best pairing

Auto-Generate Schedule

The algorithm optimizes for availability, compatibility, role coverage, fair distribution, and employee strengths.

Initializing...

Shift Configuration

Define the daily shifts. These apply to every day of the week.

🏷 Roles / Positions

Roles help the scheduler ensure proper coverage each shift.

🎯 Preset Traits

Quick-assign buttons when adding employees. Comma-separated.

💾 Data Management

Export to share or back up. Import to restore. All data lives in your browser.

`); win.document.close(); setTimeout(() => win.print(), 300); } // ===================== SETTINGS ===================== function renderSettings() { document.getElementById('shifts-config').innerHTML = data.shifts.map((s, i) => `
to
`).join(''); document.getElementById('roles-config').innerHTML = data.roles.map((r, i) => `
`).join(''); document.getElementById('preset-personality').value = data.presets.personality.join(', '); document.getElementById('preset-strengths').value = data.presets.strengths.join(', '); document.getElementById('preset-weaknesses').value = data.presets.weaknesses.join(', '); } function addShift() { const id = 's' + Date.now(); data.shifts.push({ id, name: 'New Shift', start: '09:00', end: '17:00' }); data.employees.forEach(e => { DAYS.forEach(d => { if (!data.availability[e.id]) data.availability[e.id] = {}; if (!data.availability[e.id][d]) data.availability[e.id][d] = {}; data.availability[e.id][d][id] = 'yes'; }); }); save(); renderSettings(); } function addRole() { data.roles.push('New Role'); save(); renderSettings(); } function savePresets() { data.presets.personality = document.getElementById('preset-personality').value.split(',').map(s => s.trim()).filter(Boolean); data.presets.strengths = document.getElementById('preset-strengths').value.split(',').map(s => s.trim()).filter(Boolean); data.presets.weaknesses = document.getElementById('preset-weaknesses').value.split(',').map(s => s.trim()).filter(Boolean); save(); showToast('Presets saved!', 'success'); } // ===================== DATA MANAGEMENT ===================== function exportData() { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `shiftforge_backup_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } function importData(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const imported = JSON.parse(e.target.result); if (imported.employees) { data = { ...defaultData(), ...imported }; save(); showToast('Data imported successfully!', 'success'); showPage('dashboard'); } else { showToast('Invalid file — expected ShiftForge data', 'error'); } } catch(err) { showToast('Import error: ' + err.message, 'error'); } }; reader.readAsText(file); } function confirmClearAll(btn) { const parent = btn.parentNode; const existing = parent.querySelector('.inline-confirm'); if (existing) { existing.remove(); return; } const div = document.createElement('div'); div.className = 'inline-confirm'; div.innerHTML = 'Delete everything?' + '' + ''; parent.appendChild(div); } function doClearAll() { data = defaultData(); _pendingSchedule = null; save(); showToast('All data cleared', 'warning'); showPage('dashboard'); } function exportCSV() { if (!data.currentSchedule) { showToast('No schedule to export', 'warning'); return; } const s = data.currentSchedule; let csv = 'Day,Shift,Employees\n'; DAYS.forEach(day => { if (!s.schedule[day]) return; data.shifts.forEach(shift => { const emps = (s.schedule[day][shift.id] || []).map(id => { const e = data.employees.find(x => x.id === id); return e ? e.name : 'Unknown'; }).join('; '); csv += '"' + day + '","' + shift.name + '","' + emps + '"\n'; }); }); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'shiftforge_schedule_' + new Date().toISOString().split('T')[0] + '.csv'; a.click(); URL.revokeObjectURL(url); showToast('CSV exported', 'success'); } // ===================== SCHEDULE HISTORY ===================== function renderScheduleHistory() { const card = document.getElementById('schedule-history-card'); const list = document.getElementById('schedule-history-list'); if (!data.scheduleHistory || data.scheduleHistory.length === 0) { card.style.display = 'none'; return; } card.style.display = 'block'; list.innerHTML = data.scheduleHistory.slice(0, 12).map((h, i) => { const d = new Date(h.savedAt); const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); const qColor = h.quality >= 80 ? 'var(--green)' : h.quality >= 60 ? 'var(--orange)' : 'var(--red)'; return '
' + '
' + dateStr + '
Week of ' + (h.weekStart || 'N/A') + '
' + '
' + (h.quality || 0).toFixed(0) + '%' + '' + '
'; }).join(''); } function viewHistorySchedule(idx) { const h = data.scheduleHistory[idx]; if (!h) return; _pendingSchedule = h; showPage('generate'); renderScheduleOutput(h); showToast('Viewing historical schedule', 'info'); } function restoreHistorySchedule(idx) { const h = data.scheduleHistory[idx]; if (!h) return; data.currentSchedule = h; save(); showToast('Schedule restored', 'success'); showPage('dashboard'); } // ===================== LABOR COST ===================== function renderLaborCost(schedule) { const costDiv = document.getElementById('schedule-cost'); if (!costDiv) return; let totalCost = 0; let hasRates = false; const breakdown = []; data.employees.forEach(emp => { const rate = parseFloat(data.hourlyRates[emp.id]) || 0; if (rate <= 0) return; hasRates = true; let hours = 0; DAYS.forEach(day => { if (!schedule.schedule[day]) return; data.shifts.forEach(shift => { if ((schedule.schedule[day][shift.id] || []).includes(emp.id)) { const [sh,sm] = shift.start.split(':').map(Number); const [eh,em] = shift.end.split(':').map(Number); hours += (eh + em/60) - (sh + sm/60); } }); }); const cost = hours * rate; totalCost += cost; breakdown.push({ name: emp.name, hours: hours.toFixed(1), rate: rate.toFixed(2), cost: cost.toFixed(2) }); }); if (!hasRates) { costDiv.style.display = 'none'; return; } costDiv.style.display = 'block'; costDiv.innerHTML = '
' + '💰 Estimated Labor Cost' + '$' + totalCost.toFixed(2) + '
' + '
' + breakdown.map(b => b.name + ': ' + b.hours + 'h × $' + b.rate + ' = $' + b.cost).join(' • ') + '
'; } // ===================== UTILS ===================== function esc(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } function closeModal() { document.getElementById('modal-overlay').style.display = 'none'; } // Init with hash routing (function init() { const hash = window.location.hash.slice(1); if (hash && VALID_PAGES.includes(hash)) { showPage(hash); } else { renderDashboard(); } })();