iobroker.time-tracker 0.7.8 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -421,6 +421,15 @@ local SQLite file, access is role-based, and every correction is written to an a
421
421
 
422
422
  ### **WORK IN PROGRESS**
423
423
 
424
+ ### 0.7.9 (2026-09-26)
425
+
426
+ - (Alex) fix: “Feiertag hinzufügen” stayed grey while the day had to be typed as text — it comes from the date field of
427
+ the browser now, and the label no longer names a format. A holiday entered without a region belongs to the country of
428
+ the instance instead of always `DE`, the list shows the days of **every** region of the year, and a day of the next
429
+ year takes the shown year along
430
+ - (Alex) fix: “Abbrechen” closes the form of an absence while it is being changed as well — it cleared only the flag of
431
+ the new absence and left the dialog of the change on the screen
432
+
424
433
  ### 0.7.8 (2026-09-26)
425
434
 
426
435
  - (Alex) new: an absence can be taken back. An employee withdraws a request that is still open, and for an approved
@@ -458,16 +467,6 @@ local SQLite file, access is role-based, and every correction is written to an a
458
467
  - (Alex) fix: the periodic refresh and the hourly backup check cannot overlap themselves any more. A tick that finds the previous run still busy is skipped and logged, so a slow run (a big database, a slow disk) can no longer pile a second one on top of itself
459
468
  - (Alex) change: `onStateChange` no longer writes a debug line for every state of the instance that it does not handle - on a busy system that filled the log at log level `debug` for nothing
460
469
 
461
- ### 0.7.4 (2026-09-24)
462
-
463
- - (Alex) fix: the subscription link of the company carries the API prefix now (`…/api/calendar.ics?token=…`). Without
464
- it a browser got the web app and its login instead of the calendar
465
- - (Alex) change: the written calendar file sits in `<iobroker-data>/files/time-tracker.<n>/calendar.ics` instead of the
466
- instance folder next to it — that one is readable for the adapter alone; the `ical` adapter keeps reading it as a
467
- local file
468
- - (Alex) docs: the test plan carries a small snippet for the *Scripts* tab that reads `calendar.absences` and logs who
469
- is away today (for T21, not part of the adapter)
470
-
471
470
  Older entries are kept in [`CHANGELOG_OLD.md`](CHANGELOG_OLD.md).
472
471
 
473
472
  ## Provenance
@@ -36,6 +36,7 @@ function requireDate(date) {
36
36
  }
37
37
  function createHolidaysRepository(db) {
38
38
  const selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);
39
+ const selectByYearAllRegions = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? ORDER BY date, region`);
39
40
  const selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);
40
41
  const selectYears = db.prepare("SELECT DISTINCT year FROM holidays ORDER BY year");
41
42
  const selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);
@@ -49,7 +50,10 @@ function createHolidaysRepository(db) {
49
50
  return row != null ? row : null;
50
51
  };
51
52
  return {
52
- listByYear(year, region = DEFAULT_REGION) {
53
+ listByYear(year, region) {
54
+ if (region === void 0) {
55
+ return selectByYearAllRegions.all(year);
56
+ }
53
57
  return selectByYear.all(year, region);
54
58
  },
55
59
  years() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/db/repositories/holidays.ts"],
4
- "sourcesContent": ["/**\n * Holiday repository (`holidays`).\n *\n * Holidays are stored per region, the region is the country code of the instance (`CH`, `DE`, `AT`).\n * `ensureYear` generates the holidays of a country idempotently and never touches edited rows, so\n * administrators can rename or delete single days.\n */\n\nimport type { Db } from \"../database\";\nimport { holidaysForYear, type HolidayCountry } from \"../../domain/holidays\";\nimport { isDateString } from \"../../util/time\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A public holiday as stored in the database. */\nexport interface HolidayRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Region (country code) the holiday belongs to */\n\tregion: string;\n\t/** Year of the holiday (derived from `date`) */\n\tyear: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** Display name */\n\tname: string;\n\t/** Key of a seeded holiday (`null` for one that was added by hand) */\n\tkey: string | null;\n}\n\n/** Holiday storage operations. */\nexport interface HolidaysRepository {\n\t/** Holidays of one year, sorted by date */\n\tlistByYear(year: number, region?: string): HolidayRecord[];\n\t/** Years for which holidays are stored */\n\tyears(): number[];\n\t/** True when the date is a holiday of the region */\n\tisHoliday(date: string, region?: string): boolean;\n\t/** All holiday dates of a year as a set (input for the calculation service) */\n\tdateSet(year: number, region?: string): Set<string>;\n\t/** Generates the missing holidays of a country for one year */\n\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\tyear: number;\n\t\tinserted: number;\n\t};\n\t/** Adds a holiday; an existing date of the region is renamed */\n\tadd(input: {\n\t\tdate: string;\n\t\tname: string;\n\t\tregion?: string;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): HolidayRecord;\n\t/** Deletes a holiday by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\ninterface HolidayRow {\n\tid: number;\n\tregion: string;\n\tyear: number;\n\tdate: string;\n\tname: string;\n\tkey: string | null;\n}\n\n/** Default region used when the caller does not pass one. */\nconst DEFAULT_REGION = \"DE\";\n\nconst COLUMNS = \"id, region, year, date, name, key\";\n\n/**\n * Checks and normalises a holiday date.\n *\n * @param date - date to validate\n * @returns the validated date\n */\nfunction requireDate(date: string): string {\n\tconst trimmed = date.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`invalid date \"${date}\", expected YYYY-MM-DD`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Creates the holiday repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createHolidaysRepository(db: Db): HolidaysRepository {\n\tconst selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);\n\tconst selectYears = db.prepare(\"SELECT DISTINCT year FROM holidays ORDER BY year\");\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);\n\tconst insertHoliday = db.prepare(\n\t\t`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\n\t\t ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`,\n\t);\n\tconst deleteHoliday = db.prepare(\"DELETE FROM holidays WHERE id = ?\");\n\n\tconst read = (id: number): HolidayRecord | null => {\n\t\tconst row = selectById.get(id) as HolidayRow | undefined;\n\t\treturn row ?? null;\n\t};\n\n\treturn {\n\t\tlistByYear(year: number, region: string = DEFAULT_REGION): HolidayRecord[] {\n\t\t\treturn selectByYear.all(year, region) as HolidayRecord[];\n\t\t},\n\n\t\tyears(): number[] {\n\t\t\treturn (selectYears.all() as { year: number }[]).map(row => row.year);\n\t\t},\n\n\t\tisHoliday(date: string, region: string = DEFAULT_REGION): boolean {\n\t\t\treturn selectByDate.get(date, region) !== undefined;\n\t\t},\n\n\t\tdateSet(year: number, region: string = DEFAULT_REGION): Set<string> {\n\t\t\tconst rows = selectByYear.all(year, region) as HolidayRow[];\n\t\t\treturn new Set(rows.map(row => row.date));\n\t\t},\n\n\t\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\t\tyear: number;\n\t\t\tinserted: number;\n\t\t} {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tlet inserted = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst insertMissing = db.prepare(\n\t\t\t\t\t\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t\t);\n\t\t\t\tfor (const holiday of holidaysForYear(input.year, input.country)) {\n\t\t\t\t\tinserted += insertMissing.run(\n\t\t\t\t\t\tinput.country,\n\t\t\t\t\t\tinput.year,\n\t\t\t\t\t\tholiday.date,\n\t\t\t\t\t\tholiday.name,\n\t\t\t\t\t\tholiday.key,\n\t\t\t\t\t).changes;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\n\t\t\tif (inserted > 0) {\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? null,\n\t\t\t\t\taction: \"holiday.ensure\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: input.year,\n\t\t\t\t\tdetail: { year: input.year, country: input.country, inserted },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { year: input.year, inserted };\n\t\t},\n\n\t\tadd(input: {\n\t\t\tdate: string;\n\t\t\tname: string;\n\t\t\tregion?: string;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): HolidayRecord {\n\t\t\tconst date = requireDate(input.date);\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name must not be empty\");\n\t\t\t}\n\t\t\tconst region = input.region ?? DEFAULT_REGION;\n\t\t\tconst year = Number(date.slice(0, 4));\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = selectByDate.get(date, region) as HolidayRow | undefined;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t// a day somebody adds by hand has no key: it keeps the name it was given\n\t\t\t\tinsertHoliday.run(region, year, date, name, null);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: existing ? \"holiday.update\" : \"holiday.create\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: date,\n\t\t\t\t\tdetail: existing\n\t\t\t\t\t\t? { changes: { name: { old: existing.name, new: name } } }\n\t\t\t\t\t\t: { date, name, region },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectByDate.get(date, region) as HolidayRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`holiday ${date} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst existing = read(input.id);\n\t\t\tif (!existing) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteHoliday.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"holiday.delete\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: existing.date,\n\t\t\t\t\tdetail: { region: existing.region, name: existing.name },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Holiday repository (`holidays`).\n *\n * Holidays are stored per region, the region is the country code of the instance (`CH`, `DE`, `AT`).\n * `ensureYear` generates the holidays of a country idempotently and never touches edited rows, so\n * administrators can rename or delete single days.\n */\n\nimport type { Db } from \"../database\";\nimport { holidaysForYear, type HolidayCountry } from \"../../domain/holidays\";\nimport { isDateString } from \"../../util/time\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A public holiday as stored in the database. */\nexport interface HolidayRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Region (country code) the holiday belongs to */\n\tregion: string;\n\t/** Year of the holiday (derived from `date`) */\n\tyear: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** Display name */\n\tname: string;\n\t/** Key of a seeded holiday (`null` for one that was added by hand) */\n\tkey: string | null;\n}\n\n/** Holiday storage operations. */\nexport interface HolidaysRepository {\n\t/** Holidays of one year, sorted by date; without a region the days of every region are returned */\n\tlistByYear(year: number, region?: string): HolidayRecord[];\n\t/** Years for which holidays are stored */\n\tyears(): number[];\n\t/** True when the date is a holiday of the region */\n\tisHoliday(date: string, region?: string): boolean;\n\t/** All holiday dates of a year as a set (input for the calculation service) */\n\tdateSet(year: number, region?: string): Set<string>;\n\t/** Generates the missing holidays of a country for one year */\n\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\tyear: number;\n\t\tinserted: number;\n\t};\n\t/** Adds a holiday; an existing date of the region is renamed */\n\tadd(input: {\n\t\tdate: string;\n\t\tname: string;\n\t\tregion?: string;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): HolidayRecord;\n\t/** Deletes a holiday by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\ninterface HolidayRow {\n\tid: number;\n\tregion: string;\n\tyear: number;\n\tdate: string;\n\tname: string;\n\tkey: string | null;\n}\n\n/** Default region used when the caller does not pass one. */\nconst DEFAULT_REGION = \"DE\";\n\nconst COLUMNS = \"id, region, year, date, name, key\";\n\n/**\n * Checks and normalises a holiday date.\n *\n * @param date - date to validate\n * @returns the validated date\n */\nfunction requireDate(date: string): string {\n\tconst trimmed = date.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`invalid date \"${date}\", expected YYYY-MM-DD`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Creates the holiday repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createHolidaysRepository(db: Db): HolidaysRepository {\n\tconst selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);\n\tconst selectByYearAllRegions = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? ORDER BY date, region`);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);\n\tconst selectYears = db.prepare(\"SELECT DISTINCT year FROM holidays ORDER BY year\");\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);\n\tconst insertHoliday = db.prepare(\n\t\t`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\n\t\t ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`,\n\t);\n\tconst deleteHoliday = db.prepare(\"DELETE FROM holidays WHERE id = ?\");\n\n\tconst read = (id: number): HolidayRecord | null => {\n\t\tconst row = selectById.get(id) as HolidayRow | undefined;\n\t\treturn row ?? null;\n\t};\n\n\treturn {\n\t\tlistByYear(year: number, region?: string): HolidayRecord[] {\n\t\t\t// without a region the whole year is listed: the administration also sees the days of another region,\n\t\t\t// which keeps them at hand after a change of the holiday country\n\t\t\tif (region === undefined) {\n\t\t\t\treturn selectByYearAllRegions.all(year) as HolidayRecord[];\n\t\t\t}\n\t\t\treturn selectByYear.all(year, region) as HolidayRecord[];\n\t\t},\n\n\t\tyears(): number[] {\n\t\t\treturn (selectYears.all() as { year: number }[]).map(row => row.year);\n\t\t},\n\n\t\tisHoliday(date: string, region: string = DEFAULT_REGION): boolean {\n\t\t\treturn selectByDate.get(date, region) !== undefined;\n\t\t},\n\n\t\tdateSet(year: number, region: string = DEFAULT_REGION): Set<string> {\n\t\t\tconst rows = selectByYear.all(year, region) as HolidayRow[];\n\t\t\treturn new Set(rows.map(row => row.date));\n\t\t},\n\n\t\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\t\tyear: number;\n\t\t\tinserted: number;\n\t\t} {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tlet inserted = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst insertMissing = db.prepare(\n\t\t\t\t\t\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t\t);\n\t\t\t\tfor (const holiday of holidaysForYear(input.year, input.country)) {\n\t\t\t\t\tinserted += insertMissing.run(\n\t\t\t\t\t\tinput.country,\n\t\t\t\t\t\tinput.year,\n\t\t\t\t\t\tholiday.date,\n\t\t\t\t\t\tholiday.name,\n\t\t\t\t\t\tholiday.key,\n\t\t\t\t\t).changes;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\n\t\t\tif (inserted > 0) {\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? null,\n\t\t\t\t\taction: \"holiday.ensure\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: input.year,\n\t\t\t\t\tdetail: { year: input.year, country: input.country, inserted },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { year: input.year, inserted };\n\t\t},\n\n\t\tadd(input: {\n\t\t\tdate: string;\n\t\t\tname: string;\n\t\t\tregion?: string;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): HolidayRecord {\n\t\t\tconst date = requireDate(input.date);\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name must not be empty\");\n\t\t\t}\n\t\t\tconst region = input.region ?? DEFAULT_REGION;\n\t\t\tconst year = Number(date.slice(0, 4));\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = selectByDate.get(date, region) as HolidayRow | undefined;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t// a day somebody adds by hand has no key: it keeps the name it was given\n\t\t\t\tinsertHoliday.run(region, year, date, name, null);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: existing ? \"holiday.update\" : \"holiday.create\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: date,\n\t\t\t\t\tdetail: existing\n\t\t\t\t\t\t? { changes: { name: { old: existing.name, new: name } } }\n\t\t\t\t\t\t: { date, name, region },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectByDate.get(date, region) as HolidayRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`holiday ${date} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst existing = read(input.id);\n\t\t\tif (!existing) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteHoliday.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"holiday.delete\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: existing.date,\n\t\t\t\t\tdetail: { region: existing.region, name: existing.name },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
@@ -1169,7 +1169,7 @@ function createApi(deps) {
1169
1169
  return (0, import_router.json)(200, { year, region: region != null ? region : null, holidays: holidays.listByYear(year, region) });
1170
1170
  });
1171
1171
  route("POST", "/holidays", { permission: "holiday.manage", csrf: true }, (context) => {
1172
- var _a2, _b2;
1172
+ var _a2, _b2, _c2;
1173
1173
  if (!context.auth) {
1174
1174
  throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
1175
1175
  }
@@ -1177,9 +1177,11 @@ function createApi(deps) {
1177
1177
  const created = holidays.add({
1178
1178
  date: requireString(body, "date"),
1179
1179
  name: requireString(body, "name"),
1180
- region: (_a2 = optionalString(body, "region")) != null ? _a2 : void 0,
1180
+ // without a region the day belongs to the country of the instance: the calculation looks only there, so a
1181
+ // day filed under a foreign region would never count
1182
+ region: (_b2 = (_a2 = optionalString(body, "region")) != null ? _a2 : settings.get("holiday_country")) != null ? _b2 : void 0,
1181
1183
  actorId: context.auth.user.id,
1182
- actorIp: (_b2 = context.request.remoteAddress) != null ? _b2 : null,
1184
+ actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
1183
1185
  now: now()
1184
1186
  });
1185
1187
  return (0, import_router.json)(201, { holiday: created }, { location: `/holidays/${created.id}` });