feat(kalender): Kalender mit Urlauben und Feiertagen fuer alle
Neuer Menuepunkt "Kalender" fuer alle Rollen: genehmigte Urlaube aller Mitarbeiter, auch in der Zukunft, dazu Feiertage. Monatsansicht (Standard) und Zeitleiste, filterbar nach Team und Name. - Eigene Tabelle pulse_calendar_days, gefuellt von lib/calendarSync.js im HRworks-Lauf (Fenster bis 400 Tage voraus). Buchungen entstehen weiter erst am jeweiligen Tag. - Nur validatedOk und nur freigegebene Arten (calendar_types, Haken in den HRworks-Einstellungen). Krankmeldungen werden dafuer nicht abgefragt. - GET /api/calendar fuer alle Angemeldeten, ohne Mails und Profilbilder. - Timer laeuft zusaetzlich um 12:00 Uhr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ae5a01bda
commit
c0eb00faee
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Taeglicher Abgleich der Abwesenheiten aus HRworks.
|
||||
* Abgleich der Abwesenheiten aus HRworks (morgens und mittags): erst die
|
||||
* Buchungen bis heute, dann der Kalender mit den Urlauben der Zukunft.
|
||||
*
|
||||
* Laeuft als eigener Prozess ueber einen systemd-Timer, nicht im Webdienst:
|
||||
* ein haengender Fremdaufruf soll die Anwendung nicht blockieren, und der
|
||||
@@ -8,13 +9,21 @@
|
||||
* Aufruf:
|
||||
* node src/jobs/hrworks-sync.js echter Lauf
|
||||
* node src/jobs/hrworks-sync.js --dry-run nur berichten
|
||||
* node src/jobs/hrworks-sync.js --calendar-from=2026-01-01
|
||||
* Kalender einmalig weiter zurueck fuellen
|
||||
*/
|
||||
import 'dotenv/config';
|
||||
import { initDatabase, pool } from '../db.js';
|
||||
import { getSettings } from '../lib/settings.js';
|
||||
import { syncHrworks } from '../lib/hrworksSync.js';
|
||||
import { syncCalendar } from '../lib/calendarSync.js';
|
||||
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
const calendarFrom = process.argv.find((a) => a.startsWith('--calendar-from='))?.split('=')[1];
|
||||
if (calendarFrom && !/^\d{4}-\d{2}-\d{2}$/.test(calendarFrom)) {
|
||||
console.error(`[Pulse/Kalender] --calendar-from erwartet JJJJ-MM-TT, nicht "${calendarFrom}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await initDatabase();
|
||||
@@ -23,6 +32,33 @@ try {
|
||||
console.log('[Pulse/HRworks] Abgleich ist in den Einstellungen abgeschaltet - nichts zu tun.');
|
||||
process.exit(0);
|
||||
}
|
||||
let failed = false;
|
||||
try {
|
||||
await syncBookings();
|
||||
} catch (err) {
|
||||
failed = true;
|
||||
console.error('[Pulse/HRworks] Abgleich der Buchungen fehlgeschlagen:', err.message);
|
||||
}
|
||||
// Der Kalender laeuft auch, wenn die Buchungen scheitern (z. B. Projekt archiviert).
|
||||
try {
|
||||
const cal = await syncCalendar({ dryRun, from: calendarFrom });
|
||||
console.log(
|
||||
`[Pulse/Kalender]${dryRun ? ' (Probelauf)' : ''} ${cal.from} bis ${cal.to}: ` +
|
||||
`${cal.vacation_days} Urlaubstage, ${cal.holidays} Feiertage.`
|
||||
);
|
||||
} catch (err) {
|
||||
failed = true;
|
||||
console.error('[Pulse/Kalender] Abgleich fehlgeschlagen:', err.message);
|
||||
}
|
||||
process.exit(failed ? 1 : 0);
|
||||
} catch (err) {
|
||||
console.error('[Pulse/HRworks] Abgleich fehlgeschlagen:', err.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end().catch(() => undefined);
|
||||
}
|
||||
|
||||
async function syncBookings() {
|
||||
const report = await syncHrworks({ dryRun });
|
||||
console.log(
|
||||
`[Pulse/HRworks]${dryRun ? ' (Probelauf)' : ''} ${report.from} bis ${report.to}: ` +
|
||||
@@ -39,10 +75,4 @@ try {
|
||||
for (const problem of report.failed) {
|
||||
console.warn(`[Pulse/HRworks] ${problem.ref}: ${problem.error}`);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('[Pulse/HRworks] Abgleich fehlgeschlagen:', err.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end().catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { pool, withTransaction } from '../db.js';
|
||||
import { getSettings, saveSetting } from './settings.js';
|
||||
import { addDays, today } from './dates.js';
|
||||
import { Hrworks, ABSENCE_STATES } from './hrworks.js';
|
||||
import { chunks, matchUsers } from './hrworksSync.js';
|
||||
|
||||
/*
|
||||
* Kalender aus HRworks: genehmigte Urlaube und Feiertage je Person und Tag,
|
||||
* auch weit in der Zukunft.
|
||||
*
|
||||
* Getrennt von den Buchungen (hrworksSync.js): die entstehen weiterhin erst am
|
||||
* jeweiligen Tag. Hier landet nur, was im Kalender zu sehen sein soll - und den
|
||||
* sehen alle Benutzer. Deshalb:
|
||||
* - nur Abwesenheitsarten aus `calendar_types` (Urlaub), nur `validatedOk`
|
||||
* - Krankmeldungen werden gar nicht erst abgefragt; ein Urlaubstag bleibt
|
||||
* auch dann stehen, wenn an ihm eine Krankmeldung haengt - sonst liesse
|
||||
* sich die Krankheit an der Luecke ablesen.
|
||||
*
|
||||
* Jeder Lauf ersetzt das Fenster komplett (loeschen und neu schreiben, in
|
||||
* einer Transaktion). Was davor liegt, bleibt als Rueckblick stehen.
|
||||
*/
|
||||
|
||||
/** Wie weit reichen lange Urlaube vor das Fenster zurueck? Fuer die Artabfrage. */
|
||||
const LOOKBACK_DAYS = 90;
|
||||
|
||||
/**
|
||||
* Was steht an einem Tag im Kalender? Feiertag vor Urlaub; Wochenenden und
|
||||
* freie Tage (Soll 0) tragen keinen Urlaub.
|
||||
*/
|
||||
export function kalenderEintrag(day, meta, types, typeMap = {}) {
|
||||
if (day.holiday) return { kind: 'holiday', label: day.holiday_name || 'Feiertag', half: false };
|
||||
if (!(day.regular_hours > 0)) return null;
|
||||
for (const id of day.absences) {
|
||||
const info = meta.get(id);
|
||||
if (!info || !ABSENCE_STATES.includes(info.status)) continue;
|
||||
const key = `absence:${info.type}`;
|
||||
if (!types.includes(key)) continue;
|
||||
return {
|
||||
kind: 'vacation',
|
||||
label: typeMap[key] || info.name || 'Urlaub',
|
||||
// Halber Tag: es bleibt noch Arbeitszeit uebrig.
|
||||
half: day.available_hours > 0 && day.available_hours < day.regular_hours,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {boolean} options.dryRun nichts schreiben, nur zaehlen
|
||||
* @param {string} [options.from] Standard: heute minus window_days
|
||||
* @param {string} [options.to] Standard: heute plus calendar_days_ahead
|
||||
*/
|
||||
export async function syncCalendar({ dryRun = true, from, to } = {}) {
|
||||
const { hrworks: config } = await getSettings();
|
||||
const heute = today();
|
||||
const von = from || addDays(heute, -Number(config.window_days || 60));
|
||||
const bis = to || addDays(heute, Number(config.calendar_days_ahead || 400));
|
||||
const types = Array.isArray(config.calendar_types) ? config.calendar_types.filter((t) => t.startsWith('absence:')) : [];
|
||||
|
||||
const hr = new Hrworks(config);
|
||||
const persons = await hr.persons();
|
||||
const [users] = await pool.query("SELECT id, name, email FROM pulse_users WHERE is_active = 1 AND role <> 'admin'");
|
||||
const { matched, unmatched } = matchUsers(persons, users);
|
||||
|
||||
const absences = await hr.absences(addDays(von, -LOOKBACK_DAYS), bis);
|
||||
const meta = new Map(absences.map((a) => [a.number, { type: String(a.type), name: a.name, status: a.statusIdentifier }]));
|
||||
|
||||
// Je Person und Tag ein Eintrag - auch wenn zwei HRworks-Konten auf
|
||||
// denselben Pulse-Benutzer zeigen.
|
||||
const byKey = new Map();
|
||||
const personIds = [...matched.keys()];
|
||||
for (const [start, end] of chunks(von, bis)) {
|
||||
for (const day of await hr.days(start, end, personIds)) {
|
||||
const user = matched.get(day.person);
|
||||
if (!user) continue;
|
||||
const eintrag = kalenderEintrag(day, meta, types, config.type_map);
|
||||
if (eintrag && !byKey.has(`${user.id}@${day.date}`)) {
|
||||
byKey.set(`${user.id}@${day.date}`, [user.id, day.date, eintrag.kind, eintrag.label, eintrag.half ? 1 : 0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rows = [...byKey.values()];
|
||||
|
||||
const bericht = {
|
||||
dry_run: dryRun,
|
||||
from: von,
|
||||
to: bis,
|
||||
vacation_days: rows.filter((r) => r[2] === 'vacation').length,
|
||||
holidays: rows.filter((r) => r[2] === 'holiday').length,
|
||||
unmatched_persons: unmatched,
|
||||
};
|
||||
if (dryRun) return bericht;
|
||||
|
||||
await withTransaction(async (conn) => {
|
||||
await conn.query('DELETE FROM pulse_calendar_days WHERE day BETWEEN ? AND ?', [von, bis]);
|
||||
for (let i = 0; i < rows.length; i += 500) {
|
||||
await conn.query('INSERT INTO pulse_calendar_days (user_id, day, kind, label, half) VALUES ?', [rows.slice(i, i + 500)]);
|
||||
}
|
||||
});
|
||||
|
||||
bericht.finished_at = new Date().toISOString();
|
||||
await saveSetting('calendar_status', {
|
||||
last_run: bericht.finished_at,
|
||||
vacation_days: bericht.vacation_days,
|
||||
holidays: bericht.holidays,
|
||||
});
|
||||
return bericht;
|
||||
}
|
||||
@@ -162,6 +162,9 @@ export class Hrworks {
|
||||
sick_leaves: (events.sickLeaves || []).map((e) => e?.id).filter(Boolean),
|
||||
// Feiertage tragen keine id, nur das Vorhandensein zaehlt.
|
||||
holiday: Array.isArray(events.holidays) && events.holidays.length > 0,
|
||||
// Fuer den Kalender; wie der Eintrag genau aussieht, ist nicht
|
||||
// dokumentiert - fehlt der Name, heisst er dort schlicht "Feiertag".
|
||||
holiday_name: holidayName(events.holidays),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -169,6 +172,14 @@ export class Hrworks {
|
||||
}
|
||||
}
|
||||
|
||||
export function holidayName(holidays) {
|
||||
for (const h of Array.isArray(holidays) ? holidays : []) {
|
||||
const name = typeof h === 'string' ? h : h?.name || h?.description || h?.title;
|
||||
if (typeof name === 'string' && name.trim()) return name.trim().slice(0, 160);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** { person: [{ absences: [...] }] } -> [{ person, ...absence }] */
|
||||
function flatten(data, key) {
|
||||
const out = [];
|
||||
|
||||
@@ -64,6 +64,14 @@ export const DEFAULTS = {
|
||||
'sick:KWW': 'Krankheit',
|
||||
'sick:KEK': 'Kind krank',
|
||||
},
|
||||
/*
|
||||
* Welche Abwesenheitsarten im Kalender stehen, den alle sehen. Nur
|
||||
* Urlaub - Elternzeit, Mutterschutz & Co. sagen mehr ueber eine Person,
|
||||
* als die Kollegen wissen muessen. Krankheit ist nie waehlbar.
|
||||
*/
|
||||
calendar_types: ['absence:1', 'absence:10'],
|
||||
// So weit reicht der Kalender in die Zukunft.
|
||||
calendar_days_ahead: 400,
|
||||
// Task fuer Feiertage; leer = Feiertage nicht buchen.
|
||||
holiday_task: 'Feiertag',
|
||||
// Halber Tag: feste Stundenzahl (0 = aus HRworks uebernehmen).
|
||||
@@ -71,6 +79,9 @@ export const DEFAULTS = {
|
||||
last_run: null,
|
||||
last_result: null,
|
||||
},
|
||||
// Letzter Kalenderabgleich - eigener Schluessel, damit sich die beiden
|
||||
// Laeufe ihre Ergebnisse nicht gegenseitig ueberschreiben.
|
||||
calendar_status: { last_run: null, vacation_days: 0, holidays: 0 },
|
||||
/*
|
||||
* KI-Assistent im Kopf der Anwendung. Der Schluessel liegt verschluesselt,
|
||||
* genau wie das SSO-Secret; das Modell ist einstellbar, weil OpenAI die
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import express from 'express';
|
||||
import { pool } from '../db.js';
|
||||
import { getSettings } from '../lib/settings.js';
|
||||
import { daysBetween, parseDate } from '../lib/dates.js';
|
||||
|
||||
/*
|
||||
* Kalender: Urlaube und Feiertage aller Mitarbeiter - fuer jeden Angemeldeten
|
||||
* (Absprache). Die Daten stellt der HRworks-Abgleich bereit
|
||||
* (lib/calendarSync.js); hier wird nur gelesen. Krankmeldungen stehen in der
|
||||
* Tabelle gar nicht erst.
|
||||
*/
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/** Hoechstens so viele Tage je Abfrage - eine Monatsansicht braucht 42. */
|
||||
const MAX_DAYS = 400;
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const { from, to } = req.query;
|
||||
if (!parseDate(from) || !parseDate(to) || to < from) {
|
||||
return res.status(400).json({ error: 'Zeitraum ungültig (from/to als JJJJ-MM-TT).' });
|
||||
}
|
||||
if (daysBetween(from, to) > MAX_DAYS) return res.status(400).json({ error: `Höchstens ${MAX_DAYS} Tage je Abfrage.` });
|
||||
|
||||
// Nur, was der Kalender braucht - keine Profilbilder, keine Mailadressen.
|
||||
const [users] = await pool.query(
|
||||
`SELECT id, name, color, manager_id, is_external
|
||||
FROM pulse_users WHERE is_active = 1 AND role <> 'admin' ORDER BY name`
|
||||
);
|
||||
const [days] = await pool.query(
|
||||
`SELECT c.user_id, c.day, c.kind, c.label, c.half
|
||||
FROM pulse_calendar_days c
|
||||
JOIN pulse_users u ON u.id = c.user_id AND u.is_active = 1 AND u.role <> 'admin'
|
||||
WHERE c.day BETWEEN ? AND ?
|
||||
ORDER BY c.day, c.user_id`,
|
||||
[from, to]
|
||||
);
|
||||
const { calendar_status } = await getSettings();
|
||||
res.json({
|
||||
users: users.map((u) => ({ ...u, is_external: Boolean(u.is_external) })),
|
||||
days: days.map((d) => ({ ...d, half: Boolean(d.half) })),
|
||||
last_run: calendar_status.last_run,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -3,6 +3,7 @@ import { pool } from '../db.js';
|
||||
import { getSettings } from '../lib/settings.js';
|
||||
import { Hrworks } from '../lib/hrworks.js';
|
||||
import { syncHrworks } from '../lib/hrworksSync.js';
|
||||
import { syncCalendar } from '../lib/calendarSync.js';
|
||||
|
||||
/*
|
||||
* Abwesenheiten aus HRworks - Bedienung und Kontrolle.
|
||||
@@ -16,7 +17,7 @@ const router = express.Router();
|
||||
/** Was ist eingerichtet, was hat der letzte Lauf gebracht? */
|
||||
router.get('/status', async (req, res, next) => {
|
||||
try {
|
||||
const { hrworks } = await getSettings();
|
||||
const { hrworks, calendar_status } = await getSettings();
|
||||
const [[project]] = await pool.query('SELECT id, code, name, type, start_date FROM pulse_projects WHERE code = ? LIMIT 1', [
|
||||
hrworks.project_code,
|
||||
]);
|
||||
@@ -35,6 +36,8 @@ router.get('/status', async (req, res, next) => {
|
||||
type_map: hrworks.type_map,
|
||||
last_run: hrworks.last_run,
|
||||
last_result: hrworks.last_result,
|
||||
calendar_types: hrworks.calendar_types,
|
||||
calendar: calendar_status,
|
||||
entries: Number(n),
|
||||
hours: Math.round((Number(range.minutes) / 60) * 100) / 100,
|
||||
first_date: range.von,
|
||||
@@ -77,11 +80,16 @@ router.post('/test', async (req, res, next) => {
|
||||
*/
|
||||
router.post('/sync', async (req, res, next) => {
|
||||
try {
|
||||
const bericht = await syncHrworks({
|
||||
dryRun: req.body?.apply !== true,
|
||||
from: req.body?.from,
|
||||
to: req.body?.to,
|
||||
});
|
||||
const dryRun = req.body?.apply !== true;
|
||||
const bericht = await syncHrworks({ dryRun, from: req.body?.from, to: req.body?.to });
|
||||
// Der Kalender zieht beim Uebernehmen gleich mit - er hat sein eigenes Fenster.
|
||||
if (!dryRun) {
|
||||
try {
|
||||
bericht.calendar = await syncCalendar({ dryRun: false });
|
||||
} catch (err) {
|
||||
bericht.calendar_error = err.message;
|
||||
}
|
||||
}
|
||||
res.json(bericht);
|
||||
} catch (err) {
|
||||
if (err.status) return res.status(400).json({ error: err.message });
|
||||
|
||||
@@ -118,6 +118,12 @@ router.put('/', requireRole('admin'), async (req, res, next) => {
|
||||
}
|
||||
next_.type_map = map;
|
||||
}
|
||||
// Kalender: nur Abwesenheiten, nie Krankmeldungen - den sehen alle.
|
||||
if (Array.isArray(body.hrworks.calendar_types)) {
|
||||
next_.calendar_types = [
|
||||
...new Set(body.hrworks.calendar_types.map((t) => String(t)).filter((t) => /^absence:[A-Za-z0-9_-]{1,20}$/.test(t))),
|
||||
];
|
||||
}
|
||||
// Leer = unveraendert; der geheime Teil wird nie ausgeliefert.
|
||||
if (body.hrworks.secret_access_key) next_.secret_access_key = text(body.hrworks.secret_access_key, 500).trim();
|
||||
if (body.hrworks.clear_secret) next_.secret_access_key = '';
|
||||
|
||||
@@ -164,3 +164,19 @@ CREATE TABLE IF NOT EXISTS pulse_settings (
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (k)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Kalender: genehmigte Urlaube und Feiertage aus HRworks, ein Eintrag je
|
||||
-- Person und Tag - auch in der Zukunft. Getrennt von den Buchungen, die erst
|
||||
-- am jeweiligen Tag entstehen. Krankmeldungen stehen hier bewusst nie: alle
|
||||
-- Benutzer sehen den Kalender.
|
||||
CREATE TABLE IF NOT EXISTS pulse_calendar_days (
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
day DATE NOT NULL,
|
||||
kind ENUM('vacation','holiday') NOT NULL,
|
||||
label VARCHAR(160) NOT NULL,
|
||||
half TINYINT(1) NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, day),
|
||||
KEY idx_pulse_calendar_day (day),
|
||||
CONSTRAINT fk_pulse_calendar_user FOREIGN KEY (user_id) REFERENCES pulse_users (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -16,6 +16,7 @@ import directoryRoutes from './routes/directory.js';
|
||||
import importRoutes from './routes/imports.js';
|
||||
import hrworksRoutes from './routes/hrworks.js';
|
||||
import assistantRoutes from './routes/assistant.js';
|
||||
import calendarRoutes from './routes/calendar.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = Number(process.env.PORT || 5010);
|
||||
@@ -50,6 +51,8 @@ app.use('/api/entries', requireAuth, entryRoutes);
|
||||
app.use('/api/dashboard', requireAuth, requireRole('lead'), dashboardRoutes);
|
||||
app.use('/api/reports', requireAuth, requireRole('lead'), reportRoutes);
|
||||
app.use('/api/exports', requireAuth, exportRoutes);
|
||||
// Urlaube und Feiertage - fuer alle Angemeldeten.
|
||||
app.use('/api/calendar', requireAuth, calendarRoutes);
|
||||
app.use('/api/settings', requireAuth, settingsRoutes);
|
||||
app.use('/api/directory', requireAuth, requireRole('management'), directoryRoutes);
|
||||
// Uebernahme aus Fremdsystemen - nur Administratoren.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { kalenderEintrag } from '../src/lib/calendarSync.js';
|
||||
import { holidayName } from '../src/lib/hrworks.js';
|
||||
|
||||
const tag = (extra = {}) => ({ holiday: false, holiday_name: null, absences: [], sick_leaves: [], regular_hours: 8, available_hours: 0, ...extra });
|
||||
const meta = new Map([
|
||||
['30_1', { type: '1', name: 'Urlaub', status: 'validatedOk' }],
|
||||
['30_2', { type: '1', name: 'Urlaub', status: 'requested' }],
|
||||
['30_3', { type: '3', name: 'Elternzeit', status: 'validatedOk' }],
|
||||
]);
|
||||
const types = ['absence:1'];
|
||||
|
||||
test('Kalender: nur genehmigter Urlaub der freigegebenen Arten', () => {
|
||||
assert.deepEqual(kalenderEintrag(tag({ absences: ['30_1'] }), meta, types, { 'absence:1': 'Urlaub' }), {
|
||||
kind: 'vacation',
|
||||
label: 'Urlaub',
|
||||
half: false,
|
||||
});
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['30_2'] }), meta, types), null, 'nur beantragt');
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['30_3'] }), meta, types), null, 'Art nicht freigegeben');
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['99_9'] }), meta, types), null, 'unbekannt');
|
||||
assert.equal(kalenderEintrag(tag(), meta, types), null);
|
||||
});
|
||||
|
||||
test('Kalender: Feiertag vor Urlaub, Wochenende ohne Urlaub, halbe Tage', () => {
|
||||
assert.deepEqual(kalenderEintrag(tag({ holiday: true, holiday_name: 'Tag der Deutschen Einheit', absences: ['30_1'] }), meta, types), {
|
||||
kind: 'holiday',
|
||||
label: 'Tag der Deutschen Einheit',
|
||||
half: false,
|
||||
});
|
||||
assert.equal(kalenderEintrag(tag({ holiday: true }), meta, types).label, 'Feiertag');
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['30_1'], regular_hours: 0 }), meta, types), null);
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['30_1'], available_hours: 4 }), meta, types).half, true);
|
||||
});
|
||||
|
||||
test('Kalender: eine Krankmeldung verraet sich nicht', () => {
|
||||
// Urlaub bleibt stehen, auch wenn an dem Tag eine Krankmeldung haengt ...
|
||||
assert.equal(kalenderEintrag(tag({ absences: ['30_1'], sick_leaves: ['31_9'] }), meta, types).kind, 'vacation');
|
||||
// ... und Krankheit allein erscheint nie.
|
||||
assert.equal(kalenderEintrag(tag({ sick_leaves: ['31_9'] }), meta, types), null);
|
||||
});
|
||||
|
||||
test('Feiertagsname aus unterschiedlichen Formen', () => {
|
||||
assert.equal(holidayName([{ name: 'Neujahr' }]), 'Neujahr');
|
||||
assert.equal(holidayName(['Karfreitag']), 'Karfreitag');
|
||||
assert.equal(holidayName([{}]), null);
|
||||
assert.equal(holidayName(undefined), null);
|
||||
});
|
||||
Reference in New Issue
Block a user