chore: unify name to "Meeting Mixer", README tech stack, name magic numbers (#16)
- Canonical name "Meeting Mixer" everywhere: <title>, README, .vch-description (now non-technical per convention) and package.json (meeting-mixer). - README gains a Tech-Stack section and an accurate, non-misleading port note. - Magic numbers replaced by named constants: LOW_PRIORITY_PENALTY in algorithm.js; MIN/MAX_MODERATORS_PER_DAY in utils.js, used by data.js, calendar.js and app.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3eeca50b3
commit
836d0eeda5
9 files changed
+38
-12
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
Meeting Mixer – Werkzeug zur fairen, automatischen Einteilung von Moderatoren für wiederkehrende Meetings. Es verteilt die Moderation gleichmäßig über das Team, berücksichtigt Verfügbarkeiten und Urlaube und erstellt fertige Monatspläne zum Export. Für Teams, die ihre Meeting-Moderation organisiert rotieren möchten.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Morning Meeting Planner
|
# Meeting Mixer
|
||||||
|
|
||||||
Web-App zur Planung und Verwaltung von Morning-Meeting-Moderatoren.
|
Web-App zur Planung und Verwaltung von Meeting-Moderatoren.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -12,13 +12,25 @@ Web-App zur Planung und Verwaltung von Morning-Meeting-Moderatoren.
|
|||||||
- **Verlauf** – Vergangene Monatspläne einsehen und wiederherstellen
|
- **Verlauf** – Vergangene Monatspläne einsehen und wiederherstellen
|
||||||
- **Persistenz** – Automatische Speicherung im Browser (localStorage)
|
- **Persistenz** – Automatische Speicherung im Browser (localStorage)
|
||||||
|
|
||||||
|
## Tech-Stack
|
||||||
|
|
||||||
|
- **Vanilla JavaScript (ES-Module)** – kein Framework, **kein Build-Schritt**
|
||||||
|
- **Persistenz:** Browser-`localStorage` (kein Backend, keine Datenbank)
|
||||||
|
- **Excel-Im-/Export:** [SheetJS](https://sheetjs.com/) (per CDN, mit SRI eingebunden)
|
||||||
|
- **Statisches Serving:** [`serve`](https://www.npmjs.com/package/serve) (Dev)
|
||||||
|
- **Tests:** Node-Assertions (`node:assert`), ohne externe Abhängigkeiten
|
||||||
|
|
||||||
|
Voraussetzung: ein moderner Browser mit ES-Modul-Unterstützung.
|
||||||
|
|
||||||
## Starten
|
## Starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Die App läuft auf **http://localhost:3001**.
|
Startet einen statischen Server. Der genutzte Port wird beim Start ausgegeben
|
||||||
|
(Standard 3001; weicht automatisch aus, falls belegt). Öffne anschließend die
|
||||||
|
angezeigte URL im Browser.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.sheetjs.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; object-src 'none'; base-uri 'self'">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.sheetjs.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; object-src 'none'; base-uri 'self'">
|
||||||
<title>Morning Meeting Planner</title>
|
<title>Meeting Mixer</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
import { toISO, getWeekday } from './utils.js';
|
import { toISO, getWeekday } from './utils.js';
|
||||||
|
|
||||||
|
// Score penalty applied when a candidate is low-priority on the given weekday.
|
||||||
|
const LOW_PRIORITY_PENALTY = 5;
|
||||||
|
|
||||||
function isOnVacation(employee, isoDate) {
|
function isOnVacation(employee, isoDate) {
|
||||||
return employee.constraints.vacations.some(v => v.from <= isoDate && isoDate <= v.to);
|
return employee.constraints.vacations.some(v => v.from <= isoDate && isoDate <= v.to);
|
||||||
}
|
}
|
||||||
@@ -60,7 +63,7 @@ function scoreCandidate(employee, isoDate, histCount, randomFn) {
|
|||||||
let score = -myTotal;
|
let score = -myTotal;
|
||||||
|
|
||||||
// Low priority: increase negative score (less likely to be picked)
|
// Low priority: increase negative score (less likely to be picked)
|
||||||
if (employee.constraints.lowPriorityDays.includes(wd)) score -= 5;
|
if (employee.constraints.lowPriorityDays.includes(wd)) score -= LOW_PRIORITY_PENALTY;
|
||||||
|
|
||||||
// Jitter ±0.49
|
// Jitter ±0.49
|
||||||
score += (randomFn() - 0.5) * 0.98;
|
score += (randomFn() - 0.5) * 0.98;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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';
|
||||||
import { normalizeEmployees } from './data.js';
|
import { normalizeEmployees } from './data.js';
|
||||||
|
import { MIN_MODERATORS_PER_DAY, MAX_MODERATORS_PER_DAY } from './utils.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'morning-planner-state';
|
const STORAGE_KEY = 'morning-planner-state';
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ try {
|
|||||||
if (parsed.calendar) state.calendar = parsed.calendar;
|
if (parsed.calendar) state.calendar = parsed.calendar;
|
||||||
if (Array.isArray(parsed.history)) state.history = parsed.history;
|
if (Array.isArray(parsed.history)) state.history = parsed.history;
|
||||||
if (parsed.settings && Number.isInteger(parsed.settings.moderatorsPerDay)
|
if (parsed.settings && Number.isInteger(parsed.settings.moderatorsPerDay)
|
||||||
&& parsed.settings.moderatorsPerDay >= 1 && parsed.settings.moderatorsPerDay <= 5) {
|
&& parsed.settings.moderatorsPerDay >= MIN_MODERATORS_PER_DAY
|
||||||
|
&& parsed.settings.moderatorsPerDay <= MAX_MODERATORS_PER_DAY) {
|
||||||
state.settings = { moderatorsPerDay: parsed.settings.moderatorsPerDay };
|
state.settings = { moderatorsPerDay: parsed.settings.moderatorsPerDay };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -2,7 +2,8 @@ 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';
|
||||||
import { toISO, monthPrefix, getWeekday, buildNameMap } from './utils.js';
|
import { toISO, monthPrefix, getWeekday, buildNameMap,
|
||||||
|
MIN_MODERATORS_PER_DAY, MAX_MODERATORS_PER_DAY } from './utils.js';
|
||||||
|
|
||||||
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
|
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
|
||||||
'Juli','August','September','Oktober','November','Dezember'];
|
'Juli','August','September','Oktober','November','Dezember'];
|
||||||
@@ -61,7 +62,7 @@ export function renderCalendar() {
|
|||||||
|
|
||||||
const slotsSel = document.createElement('select');
|
const slotsSel = document.createElement('select');
|
||||||
slotsSel.id = 'cal-slots';
|
slotsSel.id = 'cal-slots';
|
||||||
for (let n = 1; n <= 5; n++) {
|
for (let n = MIN_MODERATORS_PER_DAY; n <= MAX_MODERATORS_PER_DAY; n++) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = String(n);
|
opt.value = String(n);
|
||||||
opt.textContent = String(n);
|
opt.textContent = String(n);
|
||||||
|
|||||||
+4
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { MIN_MODERATORS_PER_DAY, MAX_MODERATORS_PER_DAY } from './utils.js';
|
||||||
|
|
||||||
function generateId() {
|
function generateId() {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
@@ -17,8 +19,8 @@ function generateId() {
|
|||||||
|
|
||||||
function parseSettings(raw) {
|
function parseSettings(raw) {
|
||||||
const n = raw?.moderatorsPerDay;
|
const n = raw?.moderatorsPerDay;
|
||||||
const valid = Number.isInteger(n) && n >= 1 && n <= 5;
|
const valid = Number.isInteger(n) && n >= MIN_MODERATORS_PER_DAY && n <= MAX_MODERATORS_PER_DAY;
|
||||||
return { moderatorsPerDay: valid ? n : 1 };
|
return { moderatorsPerDay: valid ? n : MIN_MODERATORS_PER_DAY };
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultConstraints() {
|
function defaultConstraints() {
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,9 @@
|
|||||||
// Shared pure helpers used across modules.
|
// Shared pure helpers and constants used across modules.
|
||||||
|
|
||||||
|
// Allowed range for the "moderators per day" setting.
|
||||||
|
export const MIN_MODERATORS_PER_DAY = 1;
|
||||||
|
export const MAX_MODERATORS_PER_DAY = 5;
|
||||||
|
|
||||||
|
|
||||||
// "YYYY-MM-DD" from numeric parts.
|
// "YYYY-MM-DD" from numeric parts.
|
||||||
export function toISO(year, month, day) {
|
export function toISO(year, month, day) {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "morning-meeting-planner",
|
"name": "meeting-mixer",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Reference in new issue
Block a user