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