Betrachter klicken ihren Tag an, wählen ihren Namen, optional Wunschperson und Wunschtag, und schreiben eine Notiz; keine Anmeldung nötig, zehn Anfragen je Stunde und Adresse. Offene Anfragen markieren den Tag im Kalender. Bearbeiter sehen sie unter „Anfragen" mit Zähler und führen den Tausch aus (über Kreuz oder Übernahme), öffnen den Tag oder lehnen ab. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
65 lines
2.1 KiB
JavaScript
65 lines
2.1 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const DEFAULT_STATE = () => ({
|
|
persons: [],
|
|
absences: [],
|
|
holidays: [],
|
|
assignments: {},
|
|
requests: [],
|
|
exported: {},
|
|
settings: {
|
|
meetingTime: '09:30',
|
|
durationMinutes: 15,
|
|
bundesland: 'BY',
|
|
calendarDisplayName: 'Morning Meeting',
|
|
inviteAttendees: false,
|
|
historyWindowDays: 60,
|
|
holidaysSeeded: false,
|
|
targetCalendarHref: '',
|
|
targetCalendarName: '',
|
|
absenceCalendarHref: '',
|
|
absenceCalendarName: '',
|
|
},
|
|
});
|
|
|
|
/** Bildtyp anhand der ersten Bytes erkennen. */
|
|
export function imageMime(buffer) {
|
|
if (!buffer || buffer.length < 12) return null;
|
|
if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg';
|
|
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return 'image/png';
|
|
if (buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') return 'image/webp';
|
|
if (buffer.subarray(0, 4).toString('ascii') === 'GIF8') return 'image/gif';
|
|
return null;
|
|
}
|
|
|
|
/** Einfacher JSON-Speicher mit atomarem Schreiben. Fotos liegen daneben im Ordner photos/. */
|
|
export function createStore(dataDir) {
|
|
const file = path.join(dataDir, 'rotatory.json');
|
|
const photoDir = path.join(dataDir, 'photos');
|
|
fs.mkdirSync(photoDir, { recursive: true });
|
|
|
|
let state = DEFAULT_STATE();
|
|
if (fs.existsSync(file)) {
|
|
const loaded = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
state = { ...state, ...loaded, settings: { ...state.settings, ...(loaded.settings || {}) } };
|
|
}
|
|
|
|
function save() {
|
|
const tmp = file + '.tmp';
|
|
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
fs.renameSync(tmp, file);
|
|
}
|
|
|
|
const photoPath = (id) => path.join(photoDir, `${id}.jpg`);
|
|
return {
|
|
get state() { return state; },
|
|
save,
|
|
photoPath,
|
|
hasPhoto: (id) => fs.existsSync(photoPath(id)),
|
|
readPhoto: (id) => (fs.existsSync(photoPath(id)) ? fs.readFileSync(photoPath(id)) : null),
|
|
savePhoto: (id, buffer) => fs.writeFileSync(photoPath(id), buffer),
|
|
deletePhoto: (id) => { try { fs.unlinkSync(photoPath(id)); } catch {} },
|
|
};
|
|
}
|