feat: auto-persist state to localStorage on every change
This commit is contained in:
28
js/app.js
28
js/app.js
@@ -2,6 +2,8 @@ import { renderEmployees } from './employees.js';
|
||||
import { renderCalendar } from './calendar.js';
|
||||
import { renderDataTab } from './history.js';
|
||||
|
||||
const STORAGE_KEY = 'morning-planner-state';
|
||||
|
||||
// Shared mutable state — imported by all other modules
|
||||
export const state = {
|
||||
employees: [],
|
||||
@@ -9,6 +11,32 @@ export const state = {
|
||||
history: []
|
||||
};
|
||||
|
||||
// Load persisted state from localStorage on startup
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed.employees)) state.employees = parsed.employees;
|
||||
if (parsed.calendar) state.calendar = parsed.calendar;
|
||||
if (Array.isArray(parsed.history)) state.history = parsed.history;
|
||||
}
|
||||
} catch {
|
||||
// Corrupt storage — start fresh
|
||||
}
|
||||
|
||||
// Call after any state mutation to persist automatically
|
||||
export function saveState() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
employees: state.employees,
|
||||
calendar: state.calendar,
|
||||
history: state.history
|
||||
}));
|
||||
} catch {
|
||||
// Storage full or unavailable — silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { state } from './app.js';
|
||||
import { state, saveState } from './app.js';
|
||||
import { getWorkdays, generatePlan } from './algorithm.js';
|
||||
import { toggleHoliday, toggleClosure, addHistoryEntries, removeHistoryEntry } from './data.js';
|
||||
import { exportSchedule, downloadBlob } from './excel.js';
|
||||
@@ -99,6 +99,7 @@ export function renderCalendar() {
|
||||
const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix));
|
||||
currentPlan = generatePlan(workdays, state.employees, historyWithoutMonth);
|
||||
addHistoryEntries(state, currentPlan);
|
||||
saveState();
|
||||
renderGrid(wrapper, exportBtn);
|
||||
});
|
||||
|
||||
@@ -184,6 +185,7 @@ function renderGrid(wrapper, exportBtn) {
|
||||
removeBtn.addEventListener('click', () => {
|
||||
if (isHoliday) toggleHoliday(state, iso);
|
||||
else toggleClosure(state, iso);
|
||||
saveState();
|
||||
renderGrid(wrapper, exportBtn);
|
||||
});
|
||||
tag.appendChild(removeBtn);
|
||||
@@ -217,6 +219,7 @@ function renderGrid(wrapper, exportBtn) {
|
||||
removeHistoryEntry(state, iso);
|
||||
currentPlan = currentPlan.filter(a => a.date !== iso);
|
||||
}
|
||||
saveState();
|
||||
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
|
||||
});
|
||||
|
||||
@@ -230,13 +233,13 @@ function renderGrid(wrapper, exportBtn) {
|
||||
ftBtn.type = 'button';
|
||||
ftBtn.textContent = 'FT';
|
||||
ftBtn.style.cssText = 'font-size:9px;padding:1px 4px';
|
||||
ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); renderGrid(wrapper, exportBtn); });
|
||||
ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); renderGrid(wrapper, exportBtn); });
|
||||
|
||||
const buBtn = document.createElement('button');
|
||||
buBtn.type = 'button';
|
||||
buBtn.textContent = 'BU';
|
||||
buBtn.style.cssText = 'font-size:9px;padding:1px 4px';
|
||||
buBtn.addEventListener('click', () => { toggleClosure(state, iso); renderGrid(wrapper, exportBtn); });
|
||||
buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); renderGrid(wrapper, exportBtn); });
|
||||
|
||||
btnRow.append(ftBtn, buBtn);
|
||||
cell.appendChild(btnRow);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { state } from './app.js';
|
||||
import { state, saveState } from './app.js';
|
||||
import { createEmployee, removeEmployee } from './data.js';
|
||||
import { readEmployeeNames } from './excel.js';
|
||||
|
||||
@@ -58,6 +58,7 @@ export function renderEmployees() {
|
||||
return;
|
||||
}
|
||||
nameInput.value = '';
|
||||
saveState();
|
||||
refreshList();
|
||||
});
|
||||
|
||||
@@ -76,6 +77,7 @@ export function renderEmployees() {
|
||||
}
|
||||
}
|
||||
e.target.value = '';
|
||||
saveState();
|
||||
refreshList();
|
||||
});
|
||||
}
|
||||
@@ -119,6 +121,7 @@ function refreshList() {
|
||||
|
||||
removeBtn.addEventListener('click', () => {
|
||||
removeEmployee(state, emp.id);
|
||||
saveState();
|
||||
refreshList();
|
||||
});
|
||||
|
||||
@@ -162,6 +165,7 @@ function renderConstraintEditor(empId) {
|
||||
const idx = arr.indexOf(day);
|
||||
if (idx >= 0) arr.splice(idx, 1); else arr.push(day);
|
||||
btn.classList.toggle('active');
|
||||
saveState();
|
||||
});
|
||||
group.appendChild(btn);
|
||||
}
|
||||
@@ -184,6 +188,7 @@ function renderConstraintEditor(empId) {
|
||||
input.addEventListener('change', () => {
|
||||
const val = input.value.trim();
|
||||
emp.constraints[constraintKey] = val === '' ? null : parseInt(val, 10);
|
||||
saveState();
|
||||
});
|
||||
row.append(lbl, input);
|
||||
return row;
|
||||
@@ -215,6 +220,7 @@ function renderConstraintEditor(empId) {
|
||||
|
||||
addVacBtn.addEventListener('click', () => {
|
||||
emp.constraints.vacations.push({ from: '', to: '' });
|
||||
saveState();
|
||||
renderVacationList(empId);
|
||||
});
|
||||
}
|
||||
@@ -240,7 +246,7 @@ function renderVacationList(empId) {
|
||||
const fromInput = document.createElement('input');
|
||||
fromInput.type = 'date';
|
||||
fromInput.value = v.from;
|
||||
fromInput.addEventListener('change', () => { emp.constraints.vacations[i].from = fromInput.value; });
|
||||
fromInput.addEventListener('change', () => { emp.constraints.vacations[i].from = fromInput.value; saveState(); });
|
||||
|
||||
const sep = document.createElement('span');
|
||||
sep.textContent = 'bis';
|
||||
@@ -248,13 +254,14 @@ function renderVacationList(empId) {
|
||||
const toInput = document.createElement('input');
|
||||
toInput.type = 'date';
|
||||
toInput.value = v.to;
|
||||
toInput.addEventListener('change', () => { emp.constraints.vacations[i].to = toInput.value; });
|
||||
toInput.addEventListener('change', () => { emp.constraints.vacations[i].to = toInput.value; saveState(); });
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.textContent = '✕';
|
||||
delBtn.addEventListener('click', () => {
|
||||
emp.constraints.vacations.splice(i, 1);
|
||||
saveState();
|
||||
renderVacationList(empId);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { state } from './app.js';
|
||||
import { state, saveState } from './app.js';
|
||||
import { loadFromJSON, saveToJSON, removeHistoryEntry } from './data.js';
|
||||
import { downloadBlob } from './excel.js';
|
||||
|
||||
@@ -79,6 +79,7 @@ export function renderDataTab() {
|
||||
state.employees = loaded.employees;
|
||||
state.calendar = loaded.calendar;
|
||||
state.history = loaded.history;
|
||||
saveState();
|
||||
alert('Daten erfolgreich geladen.');
|
||||
renderDataTab();
|
||||
} catch (err) {
|
||||
@@ -136,6 +137,7 @@ function renderHistoryTable() {
|
||||
delBtn.style.cssText = 'font-size:11px;padding:2px 6px';
|
||||
delBtn.addEventListener('click', () => {
|
||||
removeHistoryEntry(state, h.date);
|
||||
saveState();
|
||||
renderHistoryTable();
|
||||
});
|
||||
actionTd.appendChild(delBtn);
|
||||
|
||||
Reference in New Issue
Block a user