{"version":3,"file":"app-9a8b795a.f584a1446b2fc034d810.bundle.js","mappings":"AAAA","sources":["webpack://lernplattform-admin-frontend/./node_modules/moment/locale/ sync ^\\.\\/.*$","webpack://lernplattform-admin-frontend/../common/utils/MathUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/MediaBlockUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/MediaDeviceUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/MethodpoolUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/NicknameUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/PendingBlockProgressUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/QuestionUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/ShuffleUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/SkillUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/StateMutationUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/StateSelectorUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/StatisticsQueryUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/StatisticsUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/StringUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/TimeUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/TrainUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/ValidationUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/VideoBlockUtils.ts","webpack://lernplattform-admin-frontend/../common/utils/assert.ts","webpack://lernplattform-admin-frontend/../common/utils/types.ts"],"sourcesContent":["var map = {\n\t\"./af\": \"K/tc\",\n\t\"./af.js\": \"K/tc\",\n\t\"./am-et\": \"gzlh\",\n\t\"./am-et.js\": \"gzlh\",\n\t\"./ar\": \"jnO4\",\n\t\"./ar-dz\": \"o1bE\",\n\t\"./ar-dz.js\": \"o1bE\",\n\t\"./ar-kw\": \"Qj4J\",\n\t\"./ar-kw.js\": \"Qj4J\",\n\t\"./ar-ly\": \"HP3h\",\n\t\"./ar-ly.js\": \"HP3h\",\n\t\"./ar-ma\": \"CoRJ\",\n\t\"./ar-ma.js\": \"CoRJ\",\n\t\"./ar-ps\": \"TJgH\",\n\t\"./ar-ps.js\": \"TJgH\",\n\t\"./ar-sa\": \"gjCT\",\n\t\"./ar-sa.js\": \"gjCT\",\n\t\"./ar-tn\": \"bYM6\",\n\t\"./ar-tn.js\": \"bYM6\",\n\t\"./ar.js\": \"jnO4\",\n\t\"./az\": \"SFxW\",\n\t\"./az.js\": \"SFxW\",\n\t\"./be\": \"H8ED\",\n\t\"./be.js\": \"H8ED\",\n\t\"./bg\": \"hKrs\",\n\t\"./bg.js\": \"hKrs\",\n\t\"./bm\": \"p/rL\",\n\t\"./bm.js\": \"p/rL\",\n\t\"./bn\": \"kEOa\",\n\t\"./bn-bd\": \"loYQ\",\n\t\"./bn-bd.js\": \"loYQ\",\n\t\"./bn.js\": \"kEOa\",\n\t\"./bo\": \"0mo+\",\n\t\"./bo.js\": \"0mo+\",\n\t\"./br\": \"aIdf\",\n\t\"./br.js\": \"aIdf\",\n\t\"./bs\": \"JVSJ\",\n\t\"./bs.js\": \"JVSJ\",\n\t\"./ca\": \"1xZ4\",\n\t\"./ca.js\": \"1xZ4\",\n\t\"./cs\": \"PA2r\",\n\t\"./cs.js\": \"PA2r\",\n\t\"./cv\": \"A+xa\",\n\t\"./cv.js\": \"A+xa\",\n\t\"./cy\": \"l5ep\",\n\t\"./cy.js\": \"l5ep\",\n\t\"./da\": \"DxQv\",\n\t\"./da.js\": \"DxQv\",\n\t\"./de\": \"tGlX\",\n\t\"./de-at\": \"s+uk\",\n\t\"./de-at.js\": \"s+uk\",\n\t\"./de-ch\": \"u3GI\",\n\t\"./de-ch.js\": \"u3GI\",\n\t\"./de.js\": \"tGlX\",\n\t\"./dv\": \"WYrj\",\n\t\"./dv.js\": \"WYrj\",\n\t\"./el\": \"jUeY\",\n\t\"./el.js\": \"jUeY\",\n\t\"./en-au\": \"Dmvi\",\n\t\"./en-au.js\": \"Dmvi\",\n\t\"./en-ca\": \"OIYi\",\n\t\"./en-ca.js\": \"OIYi\",\n\t\"./en-gb\": \"Oaa7\",\n\t\"./en-gb.js\": \"Oaa7\",\n\t\"./en-ie\": \"4dOw\",\n\t\"./en-ie.js\": \"4dOw\",\n\t\"./en-il\": \"czMo\",\n\t\"./en-il.js\": \"czMo\",\n\t\"./en-in\": \"7C5Q\",\n\t\"./en-in.js\": \"7C5Q\",\n\t\"./en-nz\": \"b1Dy\",\n\t\"./en-nz.js\": \"b1Dy\",\n\t\"./en-sg\": \"t+mt\",\n\t\"./en-sg.js\": \"t+mt\",\n\t\"./eo\": \"Zduo\",\n\t\"./eo.js\": \"Zduo\",\n\t\"./es\": \"iYuL\",\n\t\"./es-do\": \"CjzT\",\n\t\"./es-do.js\": \"CjzT\",\n\t\"./es-mx\": \"tbfe\",\n\t\"./es-mx.js\": \"tbfe\",\n\t\"./es-us\": \"Vclq\",\n\t\"./es-us.js\": \"Vclq\",\n\t\"./es.js\": \"iYuL\",\n\t\"./et\": \"7BjC\",\n\t\"./et.js\": \"7BjC\",\n\t\"./eu\": \"D/JM\",\n\t\"./eu.js\": \"D/JM\",\n\t\"./fa\": \"jfSC\",\n\t\"./fa.js\": \"jfSC\",\n\t\"./fi\": \"gekB\",\n\t\"./fi.js\": \"gekB\",\n\t\"./fil\": \"1ppg\",\n\t\"./fil.js\": \"1ppg\",\n\t\"./fo\": \"ByF4\",\n\t\"./fo.js\": \"ByF4\",\n\t\"./fr\": \"nyYc\",\n\t\"./fr-ca\": \"2fjn\",\n\t\"./fr-ca.js\": \"2fjn\",\n\t\"./fr-ch\": \"Dkky\",\n\t\"./fr-ch.js\": \"Dkky\",\n\t\"./fr.js\": \"nyYc\",\n\t\"./fy\": \"cRix\",\n\t\"./fy.js\": \"cRix\",\n\t\"./ga\": \"USCx\",\n\t\"./ga.js\": \"USCx\",\n\t\"./gd\": \"9rRi\",\n\t\"./gd.js\": \"9rRi\",\n\t\"./gl\": \"iEDd\",\n\t\"./gl.js\": \"iEDd\",\n\t\"./gom-deva\": \"qvJo\",\n\t\"./gom-deva.js\": \"qvJo\",\n\t\"./gom-latn\": \"DKr+\",\n\t\"./gom-latn.js\": \"DKr+\",\n\t\"./gu\": \"4MV3\",\n\t\"./gu.js\": \"4MV3\",\n\t\"./he\": \"x6pH\",\n\t\"./he.js\": \"x6pH\",\n\t\"./hi\": \"3E1r\",\n\t\"./hi.js\": \"3E1r\",\n\t\"./hr\": \"S6ln\",\n\t\"./hr.js\": \"S6ln\",\n\t\"./hu\": \"WxRl\",\n\t\"./hu.js\": \"WxRl\",\n\t\"./hy-am\": \"1rYy\",\n\t\"./hy-am.js\": \"1rYy\",\n\t\"./id\": \"UDhR\",\n\t\"./id.js\": \"UDhR\",\n\t\"./is\": \"BVg3\",\n\t\"./is.js\": \"BVg3\",\n\t\"./it\": \"bpih\",\n\t\"./it-ch\": \"bxKX\",\n\t\"./it-ch.js\": \"bxKX\",\n\t\"./it.js\": \"bpih\",\n\t\"./ja\": \"B55N\",\n\t\"./ja.js\": \"B55N\",\n\t\"./jv\": \"tUCv\",\n\t\"./jv.js\": \"tUCv\",\n\t\"./ka\": \"IBtZ\",\n\t\"./ka.js\": \"IBtZ\",\n\t\"./kk\": \"bXm7\",\n\t\"./kk.js\": \"bXm7\",\n\t\"./km\": \"6B0Y\",\n\t\"./km.js\": \"6B0Y\",\n\t\"./kn\": \"PpIw\",\n\t\"./kn.js\": \"PpIw\",\n\t\"./ko\": \"Ivi+\",\n\t\"./ko.js\": \"Ivi+\",\n\t\"./ku\": \"JCF/\",\n\t\"./ku-kmr\": \"dVgr\",\n\t\"./ku-kmr.js\": \"dVgr\",\n\t\"./ku.js\": \"JCF/\",\n\t\"./ky\": \"lgnt\",\n\t\"./ky.js\": \"lgnt\",\n\t\"./lb\": \"RAwQ\",\n\t\"./lb.js\": \"RAwQ\",\n\t\"./lo\": \"sp3z\",\n\t\"./lo.js\": \"sp3z\",\n\t\"./lt\": \"JvlW\",\n\t\"./lt.js\": \"JvlW\",\n\t\"./lv\": \"uXwI\",\n\t\"./lv.js\": \"uXwI\",\n\t\"./me\": \"KTz0\",\n\t\"./me.js\": \"KTz0\",\n\t\"./mi\": \"aIsn\",\n\t\"./mi.js\": \"aIsn\",\n\t\"./mk\": \"aQkU\",\n\t\"./mk.js\": \"aQkU\",\n\t\"./ml\": \"AvvY\",\n\t\"./ml.js\": \"AvvY\",\n\t\"./mn\": \"lYtQ\",\n\t\"./mn.js\": \"lYtQ\",\n\t\"./mr\": \"Ob0Z\",\n\t\"./mr.js\": \"Ob0Z\",\n\t\"./ms\": \"6+QB\",\n\t\"./ms-my\": \"ZAMP\",\n\t\"./ms-my.js\": \"ZAMP\",\n\t\"./ms.js\": \"6+QB\",\n\t\"./mt\": \"G0Uy\",\n\t\"./mt.js\": \"G0Uy\",\n\t\"./my\": \"honF\",\n\t\"./my.js\": \"honF\",\n\t\"./nb\": \"bOMt\",\n\t\"./nb.js\": \"bOMt\",\n\t\"./ne\": \"OjkT\",\n\t\"./ne.js\": \"OjkT\",\n\t\"./nl\": \"+s0g\",\n\t\"./nl-be\": \"2ykv\",\n\t\"./nl-be.js\": \"2ykv\",\n\t\"./nl.js\": \"+s0g\",\n\t\"./nn\": \"uEye\",\n\t\"./nn.js\": \"uEye\",\n\t\"./oc-lnc\": \"Fnuy\",\n\t\"./oc-lnc.js\": \"Fnuy\",\n\t\"./pa-in\": \"8/+R\",\n\t\"./pa-in.js\": \"8/+R\",\n\t\"./pl\": \"jVdC\",\n\t\"./pl.js\": \"jVdC\",\n\t\"./ps\": \"Uzqz\",\n\t\"./ps.js\": \"Uzqz\",\n\t\"./pt\": \"8mBD\",\n\t\"./pt-br\": \"0tRk\",\n\t\"./pt-br.js\": \"0tRk\",\n\t\"./pt.js\": \"8mBD\",\n\t\"./ro\": \"lyxo\",\n\t\"./ro.js\": \"lyxo\",\n\t\"./ru\": \"lXzo\",\n\t\"./ru.js\": \"lXzo\",\n\t\"./sd\": \"Z4QM\",\n\t\"./sd.js\": \"Z4QM\",\n\t\"./se\": \"//9w\",\n\t\"./se.js\": \"//9w\",\n\t\"./si\": \"7aV9\",\n\t\"./si.js\": \"7aV9\",\n\t\"./sk\": \"e+ae\",\n\t\"./sk.js\": \"e+ae\",\n\t\"./sl\": \"gVVK\",\n\t\"./sl.js\": \"gVVK\",\n\t\"./sq\": \"yPMs\",\n\t\"./sq.js\": \"yPMs\",\n\t\"./sr\": \"zx6S\",\n\t\"./sr-cyrl\": \"E+lV\",\n\t\"./sr-cyrl.js\": \"E+lV\",\n\t\"./sr.js\": \"zx6S\",\n\t\"./ss\": \"Ur1D\",\n\t\"./ss.js\": \"Ur1D\",\n\t\"./sv\": \"X709\",\n\t\"./sv.js\": \"X709\",\n\t\"./sw\": \"dNwA\",\n\t\"./sw.js\": \"dNwA\",\n\t\"./ta\": \"PeUW\",\n\t\"./ta.js\": \"PeUW\",\n\t\"./te\": \"XLvN\",\n\t\"./te.js\": \"XLvN\",\n\t\"./tet\": \"V2x9\",\n\t\"./tet.js\": \"V2x9\",\n\t\"./tg\": \"Oxv6\",\n\t\"./tg.js\": \"Oxv6\",\n\t\"./th\": \"EOgW\",\n\t\"./th.js\": \"EOgW\",\n\t\"./tk\": \"Wv91\",\n\t\"./tk.js\": \"Wv91\",\n\t\"./tl-ph\": \"Dzi0\",\n\t\"./tl-ph.js\": \"Dzi0\",\n\t\"./tlh\": \"z3Vd\",\n\t\"./tlh.js\": \"z3Vd\",\n\t\"./tr\": \"DoHr\",\n\t\"./tr.js\": \"DoHr\",\n\t\"./tzl\": \"z1FC\",\n\t\"./tzl.js\": \"z1FC\",\n\t\"./tzm\": \"wQk9\",\n\t\"./tzm-latn\": \"tT3J\",\n\t\"./tzm-latn.js\": \"tT3J\",\n\t\"./tzm.js\": \"wQk9\",\n\t\"./ug-cn\": \"YRex\",\n\t\"./ug-cn.js\": \"YRex\",\n\t\"./uk\": \"raLr\",\n\t\"./uk.js\": \"raLr\",\n\t\"./ur\": \"UpQW\",\n\t\"./ur.js\": \"UpQW\",\n\t\"./uz\": \"Loxo\",\n\t\"./uz-latn\": \"AQ68\",\n\t\"./uz-latn.js\": \"AQ68\",\n\t\"./uz.js\": \"Loxo\",\n\t\"./vi\": \"KSF8\",\n\t\"./vi.js\": \"KSF8\",\n\t\"./x-pseudo\": \"/X5v\",\n\t\"./x-pseudo.js\": \"/X5v\",\n\t\"./yo\": \"fzPg\",\n\t\"./yo.js\": \"fzPg\",\n\t\"./zh-cn\": \"XDpg\",\n\t\"./zh-cn.js\": \"XDpg\",\n\t\"./zh-hk\": \"SatO\",\n\t\"./zh-hk.js\": \"SatO\",\n\t\"./zh-mo\": \"OmwH\",\n\t\"./zh-mo.js\": \"OmwH\",\n\t\"./zh-tw\": \"kOpN\",\n\t\"./zh-tw.js\": \"kOpN\"\n};\n\n\nfunction webpackContext(req) {\n\tvar id = webpackContextResolve(req);\n\treturn __webpack_require__(id);\n}\nfunction webpackContextResolve(req) {\n\tif(!__webpack_require__.o(map, req)) {\n\t\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\t\te.code = 'MODULE_NOT_FOUND';\n\t\tthrow e;\n\t}\n\treturn map[req];\n}\nwebpackContext.keys = function webpackContextKeys() {\n\treturn Object.keys(map);\n};\nwebpackContext.resolve = webpackContextResolve;\nmodule.exports = webpackContext;\nwebpackContext.id = \"/awd\";","export class MathUtils {\n  public static clamp(value: number, min: number, max: number): number {\n    if(min > max) {\n      return this.clamp(value, max, min);\n    }\n\n    return Math.max(min, Math.min(value, max));\n  }\n\n  /**\n   * clamps progress between 0 and 100\n   * ceils values below 99 and above 100\n   * floors values between 99 and 100\n   */\n  public static roundProgress(preciseProgressInPercent: number, clamp = true): number {\n    const rounded = (preciseProgressInPercent >= 99 && preciseProgressInPercent <= 100)\n      ? Math.floor(preciseProgressInPercent)\n      : Math.ceil(preciseProgressInPercent);\n    if(!clamp) return rounded;\n    return this.clamp(rounded, 0, 100);\n  }\n\n  public static roundToDigit(value: number, zeroCount: number): number {\n    const figure = 10 ** zeroCount;\n    return Math.max(figure / 10, Math.round(value / figure) * figure);\n  }\n\n  public static roundToPrecision(value: number, precision: number): number {\n    return Number(value.toFixed(precision));\n  }\n\n  public static lerp(from: number, to: number, x: number, deltaMs?: number): number {\n    if(deltaMs != null) return this.lerp(from, to, 1 - x ** (deltaMs * 0.001));\n    return from + (to - from) * x;\n  }\n\n  /**\n   * specifically used to round percentages when resizing a container\n   */\n  public static roundPercentForItemSize(value: number): number {\n    return Math.round(value);\n  }\n}\n","import { IPendingBlockProgress } from 'common/dto/course/progress/course.progress.interface';\n\nexport class MediaBlockUtils {\n  private static DEFAULT_MEDIA_PERCENTAGE_CONSIDERED_SEEN = 90;\n  private static SECONDS_TO_MS_FACTOR = 10;\n\n  public static checkIfPendingBlockIsFinished(\n    duration: number,\n    mediaPercentageConsideredSeen: number | null,\n    pendingBlockProgress: IPendingBlockProgress,\n  ): boolean {\n    const minMsInteractedWith = duration * this.SECONDS_TO_MS_FACTOR * (mediaPercentageConsideredSeen ?? this.DEFAULT_MEDIA_PERCENTAGE_CONSIDERED_SEEN);\n    return pendingBlockProgress.msInteractedWith >= minMsInteractedWith;\n  }\n}\n","import { DefaultBrowserBehavior } from 'amazon-chime-sdk-js';\n\nexport class MediaDeviceUtils {\n  private static browserBehaviour = new DefaultBrowserBehavior();\n\n  private constructor() {}\n\n  public static getDeviceByIdFromArray(deviceId: string, devices: Array<MediaDeviceInfo>): MediaDeviceInfo | null {\n    return devices.find(device => device.deviceId === deviceId) ?? null;\n  }\n\n  /**\n   * filters devices we don't want to show to the user - that is devices without label - except for browsers that don't support them (Chromium WebView)\n   *\n   * normally labels are omitted if the permission was denied\n   */\n  public static prepareDevicesForSelection(devices: Array<MediaDeviceInfo>): Array<MediaDeviceInfo> {\n    if(this.browserBehaviour.doesNotSupportMediaDeviceLabels()) return devices;\n\n    return devices.filter(device => device.label !== '');\n  }\n}\n","export interface ITileInfo {\n  title?: string;\n  subtitle?: string;\n  audioId?: number;\n  pictureId?: number;\n}\n\nexport class MethodpoolUtils {\n  public static parseTileText(text: string, ignoreErrors = false): Array<ITileInfo> {\n    const tileStrings = text.trim().split('\\n');\n    const tiles: Array<ITileInfo> = tileStrings.map((tileString, index) => {\n      const infos = tileString.split(';');\n      if(!ignoreErrors && infos.length !== 4) {\n        throw new Error(`Tile definition of tile ${index} must have 4 values`);\n      }\n\n      // gain robustness against legacy db entries where the fields for audioId or pictureId contain text\n      let audioId;\n      if(infos[2]) {\n        const parsedInt = parseInt(infos[2], 10);\n        audioId = Number.isNaN(parsedInt) ? undefined : parsedInt;\n      }\n      let pictureId;\n      if(infos[3]) {\n        const parsedInt = parseInt(infos[3], 10);\n        pictureId = Number.isNaN(parsedInt) ? undefined : parsedInt;\n      }\n\n      return {\n        title: infos[0]?.trim(),\n        subtitle: infos[1]?.trim(),\n        audioId,\n        pictureId,\n      };\n    });\n    return tiles;\n  }\n\n  public static parseRfgText(text: string): Array<string> {\n    return text.split(';').map(t => t.trim()).filter(t => t.length > 0);\n  }\n\n  public static stringifyTileInfos(tileInfos: Array<ITileInfo>): string {\n    return tileInfos.map(tileInfo => {\n      const orderedTileInfo = {\n        title: tileInfo.title,\n        subtitle: tileInfo.subtitle,\n        audioId: tileInfo.audioId,\n        pictureId: tileInfo.pictureId,\n      };\n      return Object.values(orderedTileInfo).join(';');\n    }).join('\\n');\n  }\n}\n","import { MIN_NICKNAME_LENGTH } from 'common/constants/nickname.constants';\nimport { ErrorType } from 'common/enum/errortype.enum';\nimport { IText } from 'common/interfaces/translations.interface';\nimport { ArrayUtils } from './ArrayUtils';\n\nexport type UserNameData = Partial<{\n  firstname?: string | null;\n  lastname?: string | null;\n  titlePrefix?: string | null;\n  titleSuffix?: string | null;\n}>;\n\nexport class NicknameUtils {\n  public static NICKNAME_TOO_SHORT_ERROR = 'ERR_NICKNAME_TOO_SHORT';\n\n  public static getFullName(\n    user: UserNameData,\n    lastnameFirst = false,\n    noNameFallback = '',\n    comma = false,\n  ): string {\n    if(noNameFallback && (user.firstname == null || user.lastname == null)) return noNameFallback;\n\n    return [\n      user.titlePrefix,\n      (lastnameFirst ? user.lastname : user.firstname),\n      (lastnameFirst ? user.firstname : user.lastname),\n      user.titleSuffix,\n    ].filter(s => s).join(comma ? ', ' : ' ');\n  }\n\n  public static handleNicknameError(errorMessage: string): IText {\n    switch(errorMessage) {\n      case this.NICKNAME_TOO_SHORT_ERROR:\n        return {\n          tk: 'common.nickname-utils.usernameToShortError',\n          tkParams: { minLength: `${MIN_NICKNAME_LENGTH}` },\n        };\n\n      case ErrorType.VALIDATION_FAILED:\n        return { tk: 'common.nickname-utils.usernameInvalidError' };\n\n      case ErrorType.NICKNAME_ALREADY_TAKEN:\n        return { tk: 'common.nickname-utils.usernameAlreadyTakenError' };\n\n      case ErrorType.BLACKLISTED_NICKNAME:\n        return { tk: 'common.nickname-utils.usernameForbiddenError' };\n\n      default:\n        return { tk: 'common.nickname-utils.usernameGeneralError' };\n    }\n  }\n\n  public static generatePromotionNickname(): string {\n    const lastParts = [\n      'Aston Martin',\n      'Audi',\n      'Bentley',\n      'BMW',\n      'Citroen',\n      'Dacia',\n      'Dodge',\n      'Fiat',\n      'Ford',\n      'Honda',\n      'Hyundai',\n      'Jaguar',\n      'Jeep',\n      'KIA',\n      'Lamborghini',\n      'Lexus',\n      'Lotus',\n      'Maserati',\n      'Mazda',\n      'Mercedes-Benz',\n      'Mitsubishi',\n      'Nissan',\n      'Opel',\n      'Peugeot',\n      'Porsche',\n      'Renault',\n      'Rolls Royce',\n      'Seat',\n      'Skoda',\n      'Suzuki',\n      'Tesla',\n      'Toyota',\n      'Volvo',\n      'VW',\n    ];\n\n    const firstParts = [\n      'Blauer',\n      'Dunkler',\n      'Gelber',\n      'Grüner',\n      'Heller',\n      'Metallic',\n      'Roter',\n      'Schneller',\n      'Schwarzer',\n      'Silberner',\n      'Weißer',\n    ];\n\n    const firstPart = ArrayUtils.getRandom(firstParts);\n    const lastPart = ArrayUtils.getRandom(lastParts);\n    return `${firstPart}-${lastPart}-${Math.floor(Math.random() * 1_000_000)}`;\n  }\n}\n","import { ICourseBlockDto } from 'common/dto/course/content/block.interface';\nimport { ICourseProgress, IPendingBlockProgress } from 'common/dto/course/progress/course.progress.interface';\n\nexport class PendingBlockProgressUtils {\n  public static generateDefaultPendingBlockProgress(\n    accessCodeId: number,\n    block: ICourseBlockDto,\n    courseProgress: ICourseProgress,\n    pendingBlockProgresses: Array<IPendingBlockProgress>,\n  ): IPendingBlockProgress {\n    const blockProgress = courseProgress.blockProgresses.find(bp => bp.blockId === block.id);\n    const pendingProgress = pendingBlockProgresses.find(bp => bp.blockId === block.id && bp.accessCodeId === accessCodeId);\n\n    return {\n      accessCodeId,\n      blockId: block.id,\n      isFinished: blockProgress ? blockProgress.isFinished : false,\n      msInteractedWith: blockProgress ? blockProgress.secondsInteractedWith * 1000 : 0,\n      currentTime: blockProgress ? blockProgress.currentTime : 0,\n      ...pendingProgress,\n      lastModified: Date.now(),\n    };\n  }\n}\n","import { LT_HELPTEXT_SEPARATOR, LT_QUESTION_SEPARATOR } from 'common/constants/quiz.constants';\nimport { IAnswerDto, IAnswerTranslationDto } from 'common/dto/course/content/answer.interface';\nimport { QuestionType } from 'common/dto/course/content/course.types';\nimport { IQuestionDto, IQuestionTranslationDto } from 'common/dto/course/content/question.interface';\nimport { IQuizAnswerDto } from 'common/dto/course/quiz/quiz.answer.interface';\nimport { IQuizQuestionDto } from 'common/dto/course/quiz/quiz.question.interface';\nimport { ExamType } from 'common/enum/quiz.enum';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { IQuestionMarkedState, IShuffledQuizAnswer, IShuffledQuizQuestion } from 'common/utils/interfaces';\nimport { ShuffleUtils } from './ShuffleUtils';\n\nexport class QuestionUtils {\n  public static parseFillInTheBlankQuestion(questionText: string): [string, string, string] {\n    const splitQuestions = this.splitFillInTheBlankQuestionIntoArray(questionText);\n    return ArrayUtils.toFixedLength(splitQuestions, 3, '') as [string, string, string];\n  }\n\n  public static parseFillInTheBlankHelpText(questionText: string): [string, string, string] {\n    const splitText = this.splitFillInTheBlankHelpTextIntoArray(questionText);\n    return ArrayUtils.toFixedLength(splitText, 3, '') as [string, string, string];\n  }\n\n  public static getCorrectAnswerIndexForFillInTheBlankQuestion(answer: IQuizAnswerDto): number {\n    if(answer.isCorrect1) return 1;\n    if(answer.isCorrect2) return 2;\n    if(answer.isCorrect3) return 3;\n    if(answer.isCorrect4) return 4;\n    return 0;\n  }\n\n  public static quizQuestionFromQuestion(question: IQuestionDto, shuffle: boolean, seed: string | null): IShuffledQuizQuestion {\n    return {\n      answers: question.answers.map((a, i) => {\n        const quizAnswer = QuestionUtils.quizAnswerFromAnswer(a);\n        return shuffle && question.type !== QuestionType.TF\n          // For LT questions the index (0,1,2) is appended to the seed. This logic must be the same as in CourseQuizService.setAnswer()\n          ? QuestionUtils.shuffleQuizAnswer(quizAnswer, `${seed}${question.answers.length > 1 ? i : ''}`)\n          : {\n            ...quizAnswer,\n            order1: 1,\n            order2: 2,\n            order3: 3,\n            order4: 4,\n          };\n      }),\n      isAnswered: false,\n      importId: question.externalId,\n      questionId: question.id,\n      parentQuestionId: question.parentQuestionId,\n      text: question.text,\n      helpText: question.helpText,\n      type: question.type,\n      audioUrl: question.audioUrl,\n      audio1Url: question.audio1Url,\n      audio2Url: question.audio2Url,\n      audio3Url: question.audio3Url,\n      audio4Url: question.audio4Url,\n      helpTextAudioUrl: question.helpTextAudioUrl,\n      pictureBlinkUrl: question.pictureBlink?.url || undefined,\n      pictureUrl: question.picture?.url || undefined,\n      pictureBlurHash: question.picture?.blurHash || undefined,\n      pictureBlinkBlurHash: question.pictureBlink?.blurHash || undefined,\n      points: question.points,\n      translations: { ...question.translations },\n    };\n  }\n\n  public static quizAnswerFromAnswer(answer: IAnswerDto): IQuizAnswerDto {\n    return {\n      ttsAudioUrl1: answer.ttsAudioUrl1,\n      ttsAudioUrl2: answer.ttsAudioUrl2,\n      ttsAudioUrl3: answer.ttsAudioUrl3,\n      ttsAudioUrl4: answer.ttsAudioUrl4,\n      audioUrl1: answer.audioUrl1,\n      audioUrl2: answer.audioUrl2,\n      audioUrl3: answer.audioUrl3,\n      audioUrl4: answer.audioUrl4,\n      text1: answer.text1,\n      text2: answer.text2,\n      text3: answer.text3,\n      text4: answer.text4,\n      isCorrect1: answer.correct1,\n      isCorrect2: answer.correct2,\n      isCorrect3: answer.correct3,\n      isCorrect4: answer.correct4,\n      isSelected1: false,\n      isSelected2: false,\n      isSelected3: false,\n      isSelected4: false,\n      pictureUrl1: answer.picture1?.url,\n      pictureUrl2: answer.picture2?.url,\n      pictureUrl3: answer.picture3?.url,\n      pictureUrl4: answer.picture4?.url,\n      pictureBlurHash1: answer.picture1?.blurHash,\n      pictureBlurHash2: answer.picture2?.blurHash,\n      pictureBlurHash3: answer.picture3?.blurHash,\n      pictureBlurHash4: answer.picture4?.blurHash,\n      translations: { ...answer.translations },\n    };\n  }\n\n  public static shuffleQuizAnswer(quizAnswer: IQuizAnswerDto, seed: string | null): IShuffledQuizAnswer {\n    const answers = this.splitQuizAnswerIntoArray(quizAnswer);\n\n    const shuffledAnswers = ShuffleUtils.shuffle(answers, seed) as FourQuizAnswers;\n\n    return this.quizAnswerFromArray(shuffledAnswers);\n  }\n\n  public static unshuffleQuizAnswer(quizAnswer: IQuizAnswerDto | IShuffledQuizAnswer): IShuffledQuizAnswer {\n    const splitAnswers = this.splitQuizAnswerIntoArray(quizAnswer);\n    const sortedAnswers = splitAnswers.sort((a, b) => a.order - b.order);\n    return this.quizAnswerFromArray(sortedAnswers);\n  }\n\n  public static extractResourcePathsFromQuestions(quizQuestions: IQuizQuestionDto[], language: string | null): { audios: string[]; images: string[] } {\n    const audios: string[] = [];\n    const images: string[] = [];\n\n    quizQuestions.forEach(q => {\n      const translatedQ = QuestionUtils.getTranslatedQuestion(q, language);\n      ArrayUtils.pushIfSet(images, q.pictureUrl, q.pictureBlinkUrl);\n      ArrayUtils.pushIfSet(audios, translatedQ.audio1Url, translatedQ.audio2Url, translatedQ.audio3Url, translatedQ.audio4Url);\n      q.answers.forEach(a => {\n        const translatedA = QuestionUtils.getTranslatedAnswer(a, language);\n        ArrayUtils.pushIfSet(images, a.pictureUrl1, a.pictureUrl2, a.pictureUrl3, a.pictureUrl4);\n        ArrayUtils.pushIfSet(audios, translatedA.ttsAudioUrl1, translatedA.ttsAudioUrl2, translatedA.ttsAudioUrl3, translatedA.ttsAudioUrl4);\n      });\n    });\n\n    return { audios, images };\n  }\n\n  public static getUpdatedAnswersFromSelectedAnswerArray<T extends IQuizQuestionDto>(question: T, selectedAnswers: number[]): T['answers'] {\n    const updatedAnswers = question.answers.map<T['answers'][number]>(a => ({ ...a }));\n\n    // LT questions\n    if(question.type === QuestionType.LT) {\n      for(let i = 0; i < updatedAnswers.length; i++) {\n        const answer = updatedAnswers[i];\n        answer.isSelected1 = selectedAnswers[i] === 1;\n        answer.isSelected2 = selectedAnswers[i] === 2;\n        answer.isSelected3 = selectedAnswers[i] === 3;\n        answer.isSelected4 = selectedAnswers[i] === 4;\n      }\n    } else {\n      // MC, SC, TF, etc.\n      const answer = updatedAnswers[0];\n      answer.isSelected1 = selectedAnswers.includes(1);\n      answer.isSelected2 = selectedAnswers.includes(2);\n      answer.isSelected3 = selectedAnswers.includes(3);\n      answer.isSelected4 = selectedAnswers.includes(4);\n    }\n\n    return updatedAnswers;\n  }\n\n  public static checkAnswers(quizAnswers: IQuizAnswerDto[]): boolean {\n    return quizAnswers.every(a => a.isCorrect1 === a.isSelected1\n      && a.isCorrect2 === a.isSelected2\n      && a.isCorrect3 === a.isSelected3\n      && a.isCorrect4 === a.isSelected4);\n  }\n\n  public static checkAnswersFromAnswerArray(question: IQuizQuestionDto, selectedAnswers: number[]): boolean {\n    return this.checkAnswers(this.getUpdatedAnswersFromSelectedAnswerArray(question, selectedAnswers));\n  }\n\n  public static splitFillInTheBlankQuestionIntoArray(questionText: string, replacementForInput = ''): string[] {\n    return this.splitFillInTheBlankTextIntoArray(questionText, LT_QUESTION_SEPARATOR, replacementForInput);\n  }\n\n  public static splitFillInTheBlankHelpTextIntoArray(questionText: string, replacementForInput = ''): string[] {\n    return this.splitFillInTheBlankTextIntoArray(questionText, LT_HELPTEXT_SEPARATOR, replacementForInput);\n  }\n\n  public static getSelectedAnswersArrayFromQuestion(question: IQuizQuestionDto): number[] {\n    return question.type === QuestionType.LT\n      // LT questions\n      ? ArrayUtils.toFixedLength(question.answers.map<number>(a => {\n        if(a.isSelected1) return 1;\n        if(a.isSelected2) return 2;\n        if(a.isSelected3) return 3;\n        if(a.isSelected4) return 4;\n        return 0;\n      }), 3, 0)\n      // MC, SC, TF, etc.\n      : this.getSelectedMcAnswersArrayFromAnswer(question.answers[0]);\n  }\n\n  public static getSelectedMcAnswersArrayFromAnswer(answer: IQuizAnswerDto): number[] {\n    const selectedAnswers: number[] = [];\n    if(answer.isSelected1) selectedAnswers.push(1);\n    if(answer.isSelected2) selectedAnswers.push(2);\n    if(answer.isSelected3) selectedAnswers.push(3);\n    if(answer.isSelected4) selectedAnswers.push(4);\n    return selectedAnswers;\n  }\n\n  /**\n   * This is currently hard coded for the official Austrian exams. Should be made configurable in the future\n   * ```\n   *                easy  medium  hard\n   * main question  1     3       5\n   * sub question   1     2       3\n   * ```\n   */\n  public static getDifficulty(newQuestion: IQuizQuestionDto): 'easy' | 'medium' | 'hard' {\n    switch(newQuestion.points) {\n      case 5: return 'hard';\n      case 3: return newQuestion.parentQuestionId ? 'hard' : 'medium';\n      case 2: return 'medium';\n      default: return 'easy';\n    }\n  }\n\n  /**\n   * @returns a list of available difficulties in point values (main questions only)\n   */\n  public static getAvailableDifficulties(questions: IQuestionDto[]): number[] {\n    const difficulties = new Set<number>();\n    for(const q of questions) {\n      if(q.parentQuestionId == null) difficulties.add(q.points);\n    }\n    return Array.from(difficulties).sort();\n  }\n\n  /**\n   * @returns the sum of all question points\n   */\n  public static getAvailablePoints<T extends IQuestionDto | IQuizQuestionDto>(questions: T[]): number {\n    return questions.reduce((sum, q) => sum + q.points, 0);\n  }\n\n  /**\n   * Depends on the subquestion being sorted as next question after the main question\n   * @param questions all questions, expected to be sorted such that each subquestion follows its main question\n   * @param questionIndex index of the main question\n   */\n  public static getSubQuestion(questions: IQuizQuestionDto[], questionIndex: number): IQuizQuestionDto | null {\n    const question = questions[questionIndex];\n    const nextQuestion = questions[questionIndex + 1];\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if(!question || !nextQuestion || nextQuestion.parentQuestionId !== question.questionId) return null;\n    return nextQuestion;\n  }\n\n  public static getMainQuestionsAndSubQuestions<T extends IQuestionDto | IQuizQuestionDto>(allQuestions: T[]): { mainQuestions: T[]; subQuestionsByQuestionId: Partial<Record<number, T>> } {\n    const subQuestionsByQuestionId = ArrayUtils.toRecord(allQuestions.filter(q => q.parentQuestionId != null), q => q.parentQuestionId ?? 0, q => q);\n    const mainQuestions = allQuestions.filter(q => q.parentQuestionId == null);\n    return { mainQuestions, subQuestionsByQuestionId };\n  }\n\n  /**\n   * TODO KL-4549 hackily checks if an exam should get the \"FS\" treatment. This doesn't even belong here :o\n   * Since KL-7378 CD95 exam types should be treated like FS types.\n   */\n  public static isFsExam(examType?: ExamType): boolean {\n    if(examType == null) return false;\n\n    // FS exams (excluding AM)\n    if(examType.startsWith('fs_') && examType !== ExamType.FS_AM) return true;\n\n    // CD95 exams (including Vienna)\n    if(examType.startsWith('c95_') || examType.startsWith('d95_')) return true;\n\n    return false;\n  }\n\n  /**\n   * checks if a question is marked in the given array. If a question has a parent question, that question is checked instead\n   */\n  public static isQuestionMarked(questionMarkedStateById: Record<number, IQuestionMarkedState>, question: IQuizQuestionDto): boolean {\n    return questionMarkedStateById[question.parentQuestionId ?? question.questionId]?.marked ?? false;\n  }\n\n  private static isShuffledQuizAnswer(quizAnswer: IQuizAnswerDto | IShuffledQuizAnswer): quizAnswer is IShuffledQuizAnswer {\n    const shuffledQuizAnswer = quizAnswer as IShuffledQuizAnswer;\n    return 'order1' in shuffledQuizAnswer\n      && 'order2' in shuffledQuizAnswer\n      && 'order3' in shuffledQuizAnswer\n      && 'order4' in shuffledQuizAnswer;\n  }\n\n  private static splitFillInTheBlankTextIntoArray(text: string, separator: string, replacementForInput = ''): string[] {\n    const regex = new RegExp(`\\\\s${separator}\\\\s?`, 'g');\n    return text.split('|').map(part => part.replace(regex, replacementForInput));\n  }\n\n  private static splitQuizAnswerTranslationsIntoArray(\n    quizAnswerTranslations: Partial<Record<string, IAnswerTranslationDto>>,\n  ): Array<Partial<Record<string, ISingleQuizAnswerTranslation>>> {\n    const splitByLanguages = Object.values(quizAnswerTranslations) as IAnswerTranslationDto[];\n    return [\n      ArrayUtils.toRecord(splitByLanguages, t => t.language, t => ({\n        text: t.text1,\n        language: t.language,\n        ttsAudioUrl: t.ttsAudioUrl1,\n      })),\n      ArrayUtils.toRecord(splitByLanguages, t => t.language, t => ({\n        text: t.text2,\n        language: t.language,\n        ttsAudioUrl: t.ttsAudioUrl2,\n      })),\n      ArrayUtils.toRecord(splitByLanguages, t => t.language, t => ({\n        text: t.text3,\n        language: t.language,\n        ttsAudioUrl: t.ttsAudioUrl3,\n      })),\n      ArrayUtils.toRecord(splitByLanguages, t => t.language, t => ({\n        text: t.text4,\n        language: t.language,\n        ttsAudioUrl: t.ttsAudioUrl4,\n      })),\n    ];\n  }\n\n  private static quizAnswerTranslationsFromArray(\n    translationArray: Array<Partial<Record<string, ISingleQuizAnswerTranslation>>>,\n  ): Partial<Record<string, IAnswerTranslationDto>> {\n    const translations: Partial<Record<string, IAnswerTranslationDto>> = {};\n\n    const languages = Array.from(new Set(Object.keys(translationArray[0])));\n\n    for(const language of languages) {\n      translations[language] = {\n        language,\n        text1: translationArray[0]?.[language]?.text ?? '',\n        text2: translationArray[1]?.[language]?.text ?? '',\n        text3: translationArray[2]?.[language]?.text ?? '',\n        text4: translationArray[3]?.[language]?.text ?? '',\n        ttsAudioUrl1: translationArray[0]?.[language]?.ttsAudioUrl ?? '',\n        ttsAudioUrl2: translationArray[1]?.[language]?.ttsAudioUrl ?? '',\n        ttsAudioUrl3: translationArray[2]?.[language]?.ttsAudioUrl ?? '',\n        ttsAudioUrl4: translationArray[3]?.[language]?.ttsAudioUrl ?? '',\n      };\n    }\n\n    return translations;\n  }\n\n  private static splitQuizAnswerIntoArray(quizAnswer: IQuizAnswerDto | IShuffledQuizAnswer): FourQuizAnswers {\n    const order = this.isShuffledQuizAnswer(quizAnswer) ? [\n      quizAnswer.order1,\n      quizAnswer.order2,\n      quizAnswer.order3,\n      quizAnswer.order4,\n    ] : [1, 2, 3, 4];\n\n    const splitTranslations = this.splitQuizAnswerTranslationsIntoArray(quizAnswer.translations);\n\n    return [\n      {\n        text: quizAnswer.text1,\n        audioUrl: quizAnswer.audioUrl1 || null,\n        ttsAudioUrl: quizAnswer.ttsAudioUrl1 || null,\n        isCorrect: quizAnswer.isCorrect1 || false,\n        isSelected: quizAnswer.isSelected1 || false,\n        pictureUrl: quizAnswer.pictureUrl1 || null,\n        pictureBlurHash: quizAnswer.pictureBlurHash1 || null,\n        order: order[0],\n        translations: splitTranslations[0],\n      },\n      {\n        text: quizAnswer.text2,\n        audioUrl: quizAnswer.audioUrl2 || null,\n        ttsAudioUrl: quizAnswer.ttsAudioUrl2 || null,\n        isCorrect: quizAnswer.isCorrect2 || false,\n        isSelected: quizAnswer.isSelected2 || false,\n        pictureUrl: quizAnswer.pictureUrl2 || null,\n        pictureBlurHash: quizAnswer.pictureBlurHash2 || null,\n        order: order[1],\n        translations: splitTranslations[1],\n      },\n      {\n        text: quizAnswer.text3,\n        audioUrl: quizAnswer.audioUrl3 || null,\n        ttsAudioUrl: quizAnswer.ttsAudioUrl3 || null,\n        isCorrect: quizAnswer.isCorrect3 || false,\n        isSelected: quizAnswer.isSelected3 || false,\n        pictureUrl: quizAnswer.pictureUrl3 || null,\n        pictureBlurHash: quizAnswer.pictureBlurHash3 || null,\n        order: order[2],\n        translations: splitTranslations[2],\n      },\n      {\n        text: quizAnswer.text4,\n        audioUrl: quizAnswer.audioUrl4 || null,\n        ttsAudioUrl: quizAnswer.ttsAudioUrl4 || null,\n        isCorrect: quizAnswer.isCorrect4 || false,\n        isSelected: quizAnswer.isSelected4 || false,\n        pictureUrl: quizAnswer.pictureUrl4 || null,\n        pictureBlurHash: quizAnswer.pictureBlurHash4 || null,\n        order: order[3],\n        translations: splitTranslations[3],\n      },\n    ];\n  }\n\n  private static quizAnswerFromArray(answers: FourQuizAnswers): IShuffledQuizAnswer {\n    return {\n      text1: answers[0].text,\n      text2: answers[1].text,\n      text3: answers[2].text,\n      text4: answers[3].text,\n      audioUrl1: answers[0].audioUrl,\n      audioUrl2: answers[1].audioUrl,\n      audioUrl3: answers[2].audioUrl,\n      audioUrl4: answers[3].audioUrl,\n      ttsAudioUrl1: answers[0].ttsAudioUrl,\n      ttsAudioUrl2: answers[1].ttsAudioUrl,\n      ttsAudioUrl3: answers[2].ttsAudioUrl,\n      ttsAudioUrl4: answers[3].ttsAudioUrl,\n      isCorrect1: answers[0].isCorrect,\n      isCorrect2: answers[1].isCorrect,\n      isCorrect3: answers[2].isCorrect,\n      isCorrect4: answers[3].isCorrect,\n      isSelected1: answers[0].isSelected,\n      isSelected2: answers[1].isSelected,\n      isSelected3: answers[2].isSelected,\n      isSelected4: answers[3].isSelected,\n      pictureUrl1: answers[0].pictureUrl,\n      pictureUrl2: answers[1].pictureUrl,\n      pictureUrl3: answers[2].pictureUrl,\n      pictureUrl4: answers[3].pictureUrl,\n      pictureBlurHash1: answers[0].pictureBlurHash,\n      pictureBlurHash2: answers[1].pictureBlurHash,\n      pictureBlurHash3: answers[2].pictureBlurHash,\n      pictureBlurHash4: answers[3].pictureBlurHash,\n      order1: answers[0].order,\n      order2: answers[1].order,\n      order3: answers[2].order,\n      order4: answers[3].order,\n      translations: this.quizAnswerTranslationsFromArray(answers.map(a => a.translations)),\n    };\n  }\n\n  public static getTranslatedQuestion<T extends IQuestionDto | IQuizQuestionDto>(question: T, language: string | null): T | IQuestionTranslationDto {\n    return (language && question.translations[language]) || question;\n  }\n\n  public static getTranslatedAnswer<T extends IAnswerDto | IQuizAnswerDto>(answer: T, language: string | null): T | IAnswerTranslationDto {\n    return (language && answer.translations[language]) || answer;\n  }\n\n  public static sanitizeQuestionText(text: string): string {\n    const regex = new RegExp(`${LT_QUESTION_SEPARATOR}`, 'g');\n    return text.replace(/\\|/g, ' ').replace(regex, '____');\n  }\n\n  public static createQuestionDto(\n    question: Exclude<Partial<IQuestionDto> & Pick<IQuestionDto, 'pageId' | 'id' | 'text' | 'type'>, 'answers'>,\n    answers: Array<Partial<IAnswerDto> & Pick<IAnswerDto, 'id' | 'text1' | 'text2' | 'text3' | 'text4' | 'correct1' | 'correct2' | 'correct3' | 'correct4'>>,\n  ): IQuestionDto {\n    return {\n      audio1Url: null,\n      audio2Url: null,\n      audio3Url: null,\n      audio4Url: null,\n      audioId: null,\n      audioUrl: null,\n      externalId: null,\n      helpText: null,\n      helpTextAudioUrl: null,\n      learningOnly: false,\n      maxMediaSeenCount: 0,\n      parentQuestionId: null,\n      picture: null,\n      pictureBlink: null,\n      points: 1,\n      translations: {},\n      videoId: null,\n      videoUrl: null,\n      ...question,\n      answers: answers.map(answer => this.createAnswerDto(answer)),\n    };\n  }\n\n  public static createAnswerDto(\n    answer: Partial<IAnswerDto> & Pick<IAnswerDto, 'id' | 'text1' | 'text2' | 'text3' | 'text4' | 'correct1' | 'correct2' | 'correct3' | 'correct4'>,\n  ): IAnswerDto {\n    return {\n      audio1Id: null,\n      audio2Id: null,\n      audio3Id: null,\n      audio4Id: null,\n      audioUrl1: null,\n      audioUrl2: null,\n      audioUrl3: null,\n      audioUrl4: null,\n      picture1: null,\n      picture2: null,\n      picture3: null,\n      picture4: null,\n      ttsAudioUrl1: null,\n      ttsAudioUrl2: null,\n      ttsAudioUrl3: null,\n      ttsAudioUrl4: null,\n      translations: {},\n      ...answer,\n    };\n  }\n\n}\n\nexport interface ISingleQuizAnswer {\n  text: string;\n  ttsAudioUrl: string | null;\n  audioUrl: string | null;\n  isCorrect: boolean;\n  isSelected: boolean;\n  pictureUrl: string | null;\n  pictureBlurHash: string | null;\n  order: number;\n  translations: Partial<Record<string, ISingleQuizAnswerTranslation>>;\n}\n\nexport interface ISingleQuizAnswerTranslation {\n  text: string;\n  ttsAudioUrl: string | null;\n  language: string;\n}\n\nexport type FourQuizAnswers = [ISingleQuizAnswer, ISingleQuizAnswer, ISingleQuizAnswer, ISingleQuizAnswer];\n","import type Seedrandom from 'seedrandom';\nimport { assertNotNullOrUndefined } from './assert';\n\nexport class ShuffleUtils {\n  public static Seedrandom: typeof Seedrandom | null = null;\n\n  public static configure(config: {\n    Seedrandom: typeof Seedrandom;\n  }): void {\n    this.Seedrandom = config.Seedrandom;\n  }\n\n  public static getRandomSeed(): string {\n    return Math.floor(Math.random() * 65000).toString();\n  }\n\n  public static getSeedFromBaseSeed(baseSeed: string, id: number): string {\n    return `${baseSeed}-${id}`;\n  }\n\n  /**\n   * Shuffle Array\n   *\n   * @param array Array to shuffle\n   */\n  public static shuffle<T>(arr: Array<T>, seed: string | null): Array<T> {\n    const resultArray = [...arr];\n    this.shuffleInPlace(resultArray, seed);\n    return resultArray;\n  }\n\n  /**\n   * Shuffle Array in place\n   *\n   * Adapted from\n   * https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array\n   * based on\n   * https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm\n   *\n   * @param array Array to shuffle in place\n   */\n  public static shuffleInPlace(array: Array<any>, seed: string | null): void {\n    if(seed === null) {\n      return;\n    }\n\n    assertNotNullOrUndefined(this.Seedrandom, 'must call ShuffleUtils.configure()');\n    const random = this.Seedrandom(seed);\n    for(let i = array.length - 1; i > 0; i--) {\n      const j = Math.floor(random() * (i + 1));\n      [array[i], array[j]] = [array[j], array[i]];\n    }\n  }\n}","import { IStudentSkillDto } from 'common/dto/skill/management/skill.management.interface';\n\nexport class SkillUtils {\n\n  public static hasSkill(\n    studentSkill: Partial<IStudentSkillDto<string>>,\n  ): boolean {\n    return studentSkill.awardedReason != null\n      && (studentSkill.expiresAt === null || (\n        studentSkill.expiresAt !== undefined && new Date(studentSkill.expiresAt) > new Date()\n      ));\n  }\n}\n","import { UniversalContent } from 'common/dto/content-editor/content.editor.interface';\nimport { ICourseBlockDto, IPresentationBlockDto } from 'common/dto/course/content/block.interface';\nimport { ICourseContentDto, IPresentationContentDto } from 'common/dto/course/content/course.interface';\nimport { BlockType } from 'common/dto/course/content/course.types';\nimport { ICourseLayoutContainerDto, ILayoutContainerDto, IPresentationLayoutContainerDto } from 'common/dto/course/layout.container.interface';\nimport {\n  ConferenceAttendeeInfoByAttendeeId,\n  ICommonState,\n  IConferenceAttendeeInfo,\n  ICourseContentWithPerformanceData,\n  ICourseContextState,\n  IPresentationContentWithPerformanceData,\n  IPresentationContextState,\n} from 'common/store/store.interface';\nimport uniq from 'lodash/uniq';\nimport { ArrayUtils } from './ArrayUtils';\nimport { BlockUtils } from './BlockUtils';\n\nexport class StateMutationUtils {\n  private constructor() { }\n\n  public static updateItemsInArray<X>(\n    currentValues: Array<X>,\n    updateInfo: Array<number> | EvaluatorFunction<X>,\n    updateCallback: (item: X, index: number) => Partial<X>,\n  ): Array<X> {\n    const indices = typeof updateInfo === 'function'\n      ? ArrayUtils.immutableSort(ArrayUtils.findIndices(currentValues, updateInfo), (a, b) => a - b)\n      : ArrayUtils.immutableSort(updateInfo, (a, b) => a - b);\n    if(indices.length === 0) return [...currentValues];\n\n    const result: Array<X> = [];\n\n    let pos = 0;\n    while(indices.length > 0) {\n      const idx: number = indices.shift()!;\n      result.push(...currentValues.slice(pos, idx));\n      result.push({\n        ...currentValues[idx],\n        ...updateCallback(currentValues[idx], idx),\n      });\n      pos = idx + 1;\n    }\n    if(pos < currentValues.length) {\n      result.push(...currentValues.slice(pos));\n    }\n\n    return result;\n  }\n\n  public static updateItemsInArrayOrdered<X>(\n    currentValues: Array<X>,\n    updateInfo: Array<number> | EvaluatorFunction<X>,\n    updateCallback: (item: X, index: number, order: number) => Partial<X>,\n    getOrderPriority: (item: X) => number,\n  ): Array<X> {\n    const indices = typeof updateInfo === 'function'\n      ? ArrayUtils.immutableSort(ArrayUtils.findIndices(currentValues, updateInfo), (a, b) => a - b)\n      : ArrayUtils.immutableSort(updateInfo, (a, b) => a - b);\n    if(indices.length === 0) return [...currentValues];\n\n    const result: Array<X> = [];\n    const indicesSortedByItemOrder = ArrayUtils.immutableSort(\n      indices,\n      (a, b) => getOrderPriority(currentValues[a]) - getOrderPriority(currentValues[b]),\n    );\n\n    let pos = 0;\n    while(indices.length > 0) {\n      const idx: number = indices.shift()!;\n      const order = indicesSortedByItemOrder.findIndex(idxByItemOrder => idxByItemOrder === idx);\n      result.push(...currentValues.slice(pos, idx));\n      result.push({\n        ...currentValues[idx],\n        ...updateCallback(currentValues[idx], idx, order),\n      });\n      pos = idx + 1;\n    }\n    if(pos < currentValues.length) {\n      result.push(...currentValues.slice(pos));\n    }\n\n    return result;\n  }\n\n  public static removeItemsInArray<X>(\n    currentValues: Array<X>,\n    removeInfo: Array<number> | EvaluatorFunction<X>,\n  ): Array<X> {\n    const indices = typeof removeInfo === 'function'\n      ? ArrayUtils.immutableSort(ArrayUtils.findIndices(currentValues, removeInfo), (a, b) => a - b)\n      : ArrayUtils.immutableSort(removeInfo, (a, b) => a - b);\n    if(indices.length === 0) return [...currentValues];\n\n    const result: Array<X> = [];\n\n    let pos = 0;\n    while(indices.length > 0) {\n      const idx: number = indices.shift()!;\n      result.push(...currentValues.slice(pos, idx));\n      pos = idx + 1;\n    }\n    if(pos < currentValues.length) {\n      result.push(...currentValues.slice(pos));\n    }\n\n    return result;\n  }\n\n  public static applyForEach<TState, TUpdateData>(originalState: TState, updates: TUpdateData[], mutation: (currentState: TState, update: TUpdateData) => TState): TState {\n    return updates.reduce(mutation, originalState);\n  }\n\n  public static updateUniversalContentForContext<TState extends ICommonState>(\n    state: TState,\n    contextIdentifier: string,\n    update: (content: UniversalContent) => Partial<UniversalContent>,\n  ): TState {\n    const context = state.contentContexts[contextIdentifier];\n    if(context == null) return { ...state };\n\n    return context.type === 'course'\n      ? StateMutationUtils.updateCourseContent(state, context.state.courseId, content => update(content) as Partial<UniversalContent<ICourseBlockDto, ICourseLayoutContainerDto>>)\n      : StateMutationUtils.updatePresentationContent(state, context.state.presentationId, content => update(content) as Partial<UniversalContent<IPresentationBlockDto, IPresentationLayoutContainerDto>>);\n  }\n\n  public static updateCourseContentForContext<TState extends ICommonState>(\n    state: TState,\n    contextIdentifier: string,\n    update: (item: ICourseContentDto) => Partial<ICourseContentDto>,\n  ): TState {\n    const context = state.contentContexts[contextIdentifier];\n    if(context == null || context.type !== 'course') return { ...state };\n\n    return StateMutationUtils.updateCourseContent(state, context.state.courseId, update);\n  }\n\n  public static updateCourseContent<TState extends ICommonState>(\n    state: TState,\n    courseId: number,\n    update: (item: ICourseContentDto) => Partial<ICourseContentDto>,\n  ): TState {\n    return this.updateCourseContentWithPerformanceData(state, courseId, update);\n  }\n\n  public static updateCourseContentWithPerformanceData<TState extends ICommonState>(\n    state: TState,\n    courseId: number,\n    update: (item: ICourseContentWithPerformanceData) => Partial<ICourseContentWithPerformanceData>,\n  ): TState {\n    const courseContent = state.courseContentById[courseId];\n    if(courseContent == null) return { ...state };\n\n    return {\n      ...state,\n      courseContentById: {\n        ...state.courseContentById,\n        [courseId]: {\n          ...courseContent,\n          ...update(courseContent),\n        },\n      },\n    };\n  }\n\n  public static updatePresentationContentForContext<TState extends ICommonState>(\n    state: TState,\n    contextIdentifier: string,\n    update: (item: IPresentationContentDto) => Partial<IPresentationContentDto>,\n  ): TState {\n    const context = state.contentContexts[contextIdentifier];\n    if(context == null || context.type !== 'presentation') return { ...state };\n\n    return StateMutationUtils.updatePresentationContent(state, context.state.presentationId, update);\n  }\n\n  public static updatePresentationContent<TState extends ICommonState>(\n    state: TState,\n    presentationId: number,\n    update: (item: IPresentationContentDto) => Partial<IPresentationContentDto>,\n  ): TState {\n    return this.updatePresentationContentWithPerformanceData(state, presentationId, update);\n  }\n\n  public static updatePresentationContentWithPerformanceData<TState extends ICommonState>(\n    state: TState,\n    presentationId: number,\n    update: (item: IPresentationContentWithPerformanceData) => Partial<IPresentationContentWithPerformanceData>,\n  ): TState {\n    const presentationContent = state.presentationContentById[presentationId];\n    if(presentationContent == null) return { ...state };\n\n    return {\n      ...state,\n      presentationContentById: {\n        ...state.presentationContentById,\n        [presentationId]: {\n          ...presentationContent,\n          ...update(presentationContent),\n        },\n      },\n    };\n  }\n\n  public static updatePresentationContextState<TState extends ICommonState>(\n    state: TState,\n    contextIdentifier: string,\n    update: (item: IPresentationContextState | null) => Partial<IPresentationContextState>,\n  ): TState {\n    const context = state.contentContexts[contextIdentifier];\n    if(context != null && context.type !== 'presentation') return { ...state };\n\n    return {\n      ...state,\n      contentContexts: {\n        ...state.contentContexts,\n        [contextIdentifier]: {\n          ...context,\n          type: 'presentation',\n          state: {\n            ...context?.state,\n            ...update(context?.state ?? null),\n          },\n        },\n      },\n    };\n  }\n\n  public static updateCourseContextState<TState extends ICommonState>(\n    state: TState,\n    contextIdentifier: string,\n    update: (item: ICourseContextState | null) => Partial<ICourseContextState>,\n  ): TState {\n    const context = state.contentContexts[contextIdentifier];\n    if(context != null && context.type !== 'course') return { ...state };\n\n    return {\n      ...state,\n      contentContexts: {\n        ...state.contentContexts,\n        [contextIdentifier]: {\n          ...context,\n          type: 'course',\n          state: {\n            ...context?.state,\n            ...update(context?.state ?? null),\n          },\n        },\n      },\n    };\n  }\n\n  public static updateContentPerformanceData<TState extends ICommonState>(\n    state: TState,\n    contentType: 'course' | 'presentation',\n    contentId: number,\n  ): TState {\n    const content = contentType === 'course'\n      ? state.courseContentById[contentId]\n      : state.presentationContentById[contentId];\n    if(content == null) return { ...state };\n\n    const { layoutContainers }: { layoutContainers: Array<ILayoutContainerDto> } = content;\n    const { blocks }: { blocks: Array<ICourseBlockDto | IPresentationBlockDto> } = content;\n\n    const layoutContainerIndexById = ArrayUtils.toRecord<number, ILayoutContainerDto, number>(layoutContainers, l => l.id, (l, _, index) => index);\n    const blockIndexById = ArrayUtils.toRecord<number, ICourseBlockDto | IPresentationBlockDto, number>(blocks, b => b.id, (b, _, index) => index);\n\n    const orderedLayoutContainerIdsByParentId = layoutContainers\n      .reduce<{ [containerId: number]: Array<number> }>((acc, layoutContainer) => {\n        const orderedChildContainerIds = layoutContainers\n          .filter(container => container.parentLayoutContainerId === layoutContainer.id)\n          .sort((a, b) => a.order - b.order)\n          .map(container => container.id);\n\n        return { ...acc, [layoutContainer.id]: orderedChildContainerIds };\n      }, {});\n\n    const getBlockRatio = ({ type: blockType, picture, pictureBlockRenderOptions }: ICourseBlockDto | IPresentationBlockDto): number | null => {\n      if(blockType === BlockType.VIDEO || blockType === BlockType.VIMEO || blockType === BlockType.YOUTUBE) {\n        return 16 / 9;\n      } else if(blockType === BlockType.PICTURE && picture != null && picture.width != null && picture.height != null) {\n        return BlockUtils.getPictureBlockRatio(picture, pictureBlockRenderOptions);\n      }\n\n      return null;\n    };\n\n    const blockPerformanceDataById = ArrayUtils.toRecord(blocks, b => b.id, b => ({\n      isTemplateBlock: BlockUtils.isTemplateBlock(b),\n      ratio: getBlockRatio(b),\n    }));\n\n    if(contentType === 'course') {\n      const courseContent = content as ICourseContentWithPerformanceData;\n\n      const rootLayoutContainerIdBySlideId = courseContent.iasSlides\n        .reduce<{ [slideId: number]: number }>((acc, slide) => {\n          const rootLayoutContainerId = courseContent.layoutContainers\n            .find(container => container.slideId === slide.id && container.parentLayoutContainerId == null)?.id ?? null;\n          if(rootLayoutContainerId == null) return acc;\n\n          return { ...acc, [slide.id]: rootLayoutContainerId };\n        }, {});\n\n      const rootLayoutContainerIdByPageId = courseContent.pages\n        .reduce<{ [pageId: number]: number }>((acc, page) => {\n          const rootLayoutContainerId = courseContent.layoutContainers\n            .find(container => container.pageId === page.id && container.slideId == null && container.parentLayoutContainerId == null)?.id ?? null;\n          if(rootLayoutContainerId == null) return acc;\n\n          return { ...acc, [page.id]: rootLayoutContainerId };\n        }, {});\n\n      const getPageIdsForParentId = (parentId: number | null): Array<number> => {\n        const sortedSubPages = courseContent.pages\n          .filter(page => page.parentId === parentId && !page.isTrainingOnly)\n          .sort((a, b) => a.order - b.order);\n\n        return sortedSubPages.reduce<Array<number>>((acc, page) => ([\n          ...acc,\n          ...(page.type === 'page'\n            ? [page.id]\n            : getPageIdsForParentId(page.id)),\n        ]), []);\n      };\n\n      const orderedPageIds = getPageIdsForParentId(null);\n\n      const rootPageId = orderedPageIds[0] ?? null;\n\n      const orderedIasSlideIdsByPageId = uniq(courseContent.iasSlides.map(slide => slide.pageId))\n        .reduce((acc, pageId) => {\n          const pageSlides = ArrayUtils.immutableSort(\n            courseContent.iasSlides.filter(slide => slide.pageId === pageId),\n            (a, b) => a.order - b.order,\n          );\n          if(pageSlides.length === 0) return acc;\n\n          return {\n            ...acc,\n            [pageId]: pageSlides.map(slide => slide.id),\n          };\n        }, {});\n\n      const questionIdByBlockId = (blocks as Array<ICourseBlockDto>)\n        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        .filter(block => block.type === BlockType.MP_QUESTION && block.questionIds[0] != null)\n        .reduce((acc, block) => ({\n          ...acc,\n          [block.id]: block.questionIds[0],\n        }), {});\n\n      const pageIdByBlockId = ArrayUtils.toRecord(blocks as Array<ICourseBlockDto>, b => b.id, b => b.pageId);\n\n      return StateMutationUtils.updateCourseContentWithPerformanceData(state, contentId, () => ({\n        orderedLayoutContainerIdsByParentId,\n        rootLayoutContainerIdBySlideId,\n        rootLayoutContainerIdByPageId,\n        orderedPageIds,\n        rootPageId,\n        orderedIasSlideIdsByPageId,\n        questionIdByBlockId,\n        pageIdByBlockId,\n        blockIndexById,\n        layoutContainerIndexById,\n        blockPerformanceDataById,\n      }));\n    }\n\n    const presentationContent = content as IPresentationContentWithPerformanceData;\n\n    const rootLayoutContainerIdBySlideId = presentationContent.slides.reduce<{ [slideId: number]: number }>((acc, slide) => {\n      const rootLayoutContainerId = presentationContent.layoutContainers\n        .find(container => container.slideId === slide.id && container.parentLayoutContainerId == null)?.id ?? null;\n      if(rootLayoutContainerId == null) return acc;\n\n      return { ...acc, [slide.id]: rootLayoutContainerId };\n    }, {});\n\n    const rootSlideId = presentationContent.slides\n      .find(slide1 => slide1.order === Math.min(...presentationContent.slides.map(slide2 => slide2.order)))?.id ?? null;\n\n    const orderedSlideIds = [...presentationContent.slides]\n      .sort((a, b) => a.order - b.order)\n      .map(slide => slide.id);\n\n    return StateMutationUtils.updatePresentationContentWithPerformanceData(state, contentId, () => ({\n      orderedLayoutContainerIdsByParentId,\n      rootLayoutContainerIdBySlideId,\n      rootSlideId,\n      orderedSlideIds,\n      blockIndexById,\n      layoutContainerIndexById,\n      blockPerformanceDataById,\n    }));\n  }\n\n  public static updateConferencePerformanceData<TState extends ICommonState>(\n    state: TState,\n    updateOptions: Partial<IUpdateConferencePerformanceDataOptions>,\n  ): TState {\n    const options: IUpdateConferencePerformanceDataOptions = {\n      updateActiveSpeakerPerformanceData: false,\n      updateTilePerformanceData: false,\n      updateAttendeeInfoPerformanceData: false,\n      ...updateOptions,\n    };\n\n    if(\n      !options.updateActiveSpeakerPerformanceData\n      && !options.updateAttendeeInfoPerformanceData\n      && !options.updateTilePerformanceData\n    ) return state;\n\n    const contentShareTileIdByAttendeeId = options.updateTilePerformanceData\n      ? state.conferenceContext.tiles.filter(tile => tile.isContent)\n        .reduce<{ [chimeAttendeeId: string]: number }>((acc, tile) => ({ ...acc, [tile.boundAttendeeId]: tile.tileId }), {})\n      : state.conferenceContext.contentShareTileIdByAttendeeId;\n\n    const videoTileIdByAttendeeId = options.updateTilePerformanceData\n      ? state.conferenceContext.tiles.filter(tile => !tile.isContent)\n        .reduce<{ [chimeAttendeeId: string]: number }>((acc, tile) => ({ ...acc, [tile.boundAttendeeId]: tile.tileId }), {})\n      : state.conferenceContext.videoTileIdByAttendeeId;\n\n    const isActiveSpeakerByAttendeeId = options.updateActiveSpeakerPerformanceData\n      ? state.conferenceContext.activeSpeakers.reduce<{ [chimeAttendeeId: string]: boolean }>((acc, attendeeId) => ({ ...acc, [attendeeId]: true }), {})\n      : state.conferenceContext.isActiveSpeakerByAttendeeId;\n\n    const attendeeInfoByAttendeeId = options.updateAttendeeInfoPerformanceData\n      ? StateMutationUtils.getUpdatedConferenceAttendeeInfoByAttendeeId(state)\n      : state.conferenceContext.attendeeInfoByAttendeeId;\n\n    return {\n      ...state,\n      conferenceContext: {\n        ...state.conferenceContext,\n        contentShareTileIdByAttendeeId,\n        videoTileIdByAttendeeId,\n        isActiveSpeakerByAttendeeId,\n        attendeeInfoByAttendeeId,\n      },\n    };\n  }\n\n  private static getUpdatedConferenceAttendeeInfoByAttendeeId<TState extends ICommonState>(state: TState): ConferenceAttendeeInfoByAttendeeId {\n    const allAttendeeData = StateMutationUtils.collectAllAttendeeData(state);\n    const attendeeInfoByAttendeeId = {\n      ...state.conferenceContext.attendeeInfoByAttendeeId,\n    };\n\n    for(const [chimeAttendeeId, attendeeInfo] of Object.entries(state.conferenceContext.attendeeInfoByAttendeeId)) {\n      const foundAttendeeData = allAttendeeData.find(attendeeData => attendeeData.chimeAttendeeId === chimeAttendeeId) ?? null;\n\n      if(foundAttendeeData == null) {\n        // delete keys without attendee\n        delete attendeeInfoByAttendeeId[chimeAttendeeId];\n      } else if(\n        attendeeInfo?.avatarSvgDownloadLink !== foundAttendeeData.avatarSvgDownloadLink\n        || attendeeInfo.firstname !== foundAttendeeData.firstname\n        || attendeeInfo.lastname !== foundAttendeeData.lastname\n        || attendeeInfo.nickname !== foundAttendeeData.nickname\n        || attendeeInfo.points !== foundAttendeeData.points\n        || attendeeInfo.userId !== foundAttendeeData.userId\n        || attendeeInfo.titlePrefix !== foundAttendeeData.titlePrefix\n        || attendeeInfo.titleSuffix !== foundAttendeeData.titleSuffix\n      ) {\n        // update keys when data has changed\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { chimeAttendeeId: _, ...conferenceAttendee } = foundAttendeeData;\n        attendeeInfoByAttendeeId[chimeAttendeeId] = conferenceAttendee;\n      }\n    }\n\n    // add not existing keys\n    for(const attendeeData of allAttendeeData.filter(x => attendeeInfoByAttendeeId[x.chimeAttendeeId] == null)) {\n      const { chimeAttendeeId, ...conferenceAttendee } = attendeeData;\n      attendeeInfoByAttendeeId[chimeAttendeeId] = conferenceAttendee;\n    }\n\n    return attendeeInfoByAttendeeId;\n  }\n\n  private static collectAllAttendeeData<TState extends ICommonState>(state: TState): Array<ConferenceAttendeeWithChimeAttendeeId> {\n    const attendees: Array<ConferenceAttendeeWithChimeAttendeeId> = [];\n\n    for(const attendee of state.distinar.attendees) {\n      if(attendee.chimeAttendeeId == null) continue;\n\n      attendees.push({\n        chimeAttendeeId: attendee.chimeAttendeeId,\n        avatarSvgDownloadLink: attendee.avatarSvgDownloadLink,\n        firstname: 'firstname' in attendee ? attendee.firstname : null,\n        lastname: 'lastname' in attendee ? attendee.lastname : null,\n        nickname: attendee.nickname,\n        points: attendee.points,\n        titlePrefix: null,\n        titleSuffix: null,\n        userId: attendee.id,\n      });\n    }\n\n    const lecturerChimeAttendeeId = state.distinar.lectureStatus?.conference.chimeLecturerAttendeeId ?? null;\n    const lecturerInfo = state.distinar.activeLecture?.lecturer ?? null;\n    if(lecturerChimeAttendeeId != null && lecturerInfo != null) {\n      attendees.push({\n        chimeAttendeeId: lecturerChimeAttendeeId,\n        avatarSvgDownloadLink: null,\n        firstname: lecturerInfo.firstname ?? null,\n        lastname: lecturerInfo.lastname ?? null,\n        nickname: null,\n        points: null,\n        titlePrefix: lecturerInfo.titlePrefix ?? null,\n        titleSuffix: lecturerInfo.titleSuffix ?? null,\n        userId: lecturerInfo.id,\n      });\n    }\n\n    return attendees;\n  }\n}\n\ntype ConferenceAttendeeWithChimeAttendeeId = IConferenceAttendeeInfo & { chimeAttendeeId: string };\n\ninterface IUpdateConferencePerformanceDataOptions {\n  updateTilePerformanceData: boolean;\n  updateActiveSpeakerPerformanceData: boolean;\n  updateAttendeeInfoPerformanceData: boolean;\n}\n\ntype EvaluatorFunction<T> = (item: T) => boolean;\n","import { IUniversalContentWithPerformanceData, IUniversalContentSelectorBlockPerformanceData } from 'common/store/store.interface';\n\nexport class StateSelectorUtils {\n  private constructor() { }\n\n  public static selectBlock<TBlock>(content: IUniversalContentWithPerformanceData<TBlock> | null | undefined, blockId: number | null | undefined): TBlock | null {\n    const blockIndex = blockId != null ? content?.blockIndexById[blockId] : undefined;\n    const block = blockIndex != null ? content?.blocks[blockIndex] : undefined;\n    return block ?? null;\n  }\n\n  public static selectLayoutContainer<TLayoutContainer>(content: IUniversalContentWithPerformanceData<any, TLayoutContainer> | null | undefined, layoutContainerId: number | null | undefined): TLayoutContainer | null {\n    const layoutContainerIndex = layoutContainerId != null ? content?.layoutContainerIndexById[layoutContainerId] : undefined;\n    const layoutContainer = layoutContainerIndex != null ? content?.layoutContainers[layoutContainerIndex] : undefined;\n    return layoutContainer ?? null;\n  }\n\n  public static selectBlockByLayoutContainerId<TBlock>(content: IUniversalContentWithPerformanceData<TBlock> | null | undefined, layoutContainerId: number | null | undefined): TBlock | null {\n    const layoutContainer = this.selectLayoutContainer(content, layoutContainerId);\n    return this.selectBlock(content, layoutContainer?.blockId);\n  }\n\n  public static selectBlockPerformanceData<TBlock>(content: IUniversalContentWithPerformanceData<TBlock> | null | undefined, blockId: number | null | undefined): IUniversalContentSelectorBlockPerformanceData | null {\n    const blockPerformanceData = blockId != null ? content?.blockPerformanceDataById[blockId] : undefined;\n    return blockPerformanceData ?? null;\n  }\n}\n","import { IFilterDto } from 'common/dto/filter/filter.interface';\nimport { \n  IAccessCodeStatisticsDurationsOrPointsQueryDto,\n  IAccessCodeStatisticsUsersAndSessionsQueryDto,\n} from 'common/dto/statistics/access-code/access-code.statistics.interface';\nimport { FilterOperationType } from 'common/dto/filter/filter.enum';\nimport { \n  AccessCodeStatisticsDurationsOrPointsType,\n  AccessCodeStatisticsDurationsOrPoints,\n  AccessCodeStatisticsUsersAndSessionsType,\n} from 'common/enum/statistics.enum';\n\ntype VirtualFilterField = any;\n\nexport class StatisticsQueryUtils {\n  private constructor() { }\n\n  public static addUsersAndSessionsTypeFilter(\n    existingFilterDto: IFilterDto,\n    usersAndSessionsType?: AccessCodeStatisticsUsersAndSessionsType,\n  ): IFilterDto {\n    return {\n      ...existingFilterDto,\n      filter: [\n        ...(existingFilterDto.filter ? existingFilterDto.filter : []),\n        ...(usersAndSessionsType ? [{ \n          field: 'usersAndSessionsType' as VirtualFilterField, \n          type: FilterOperationType.Eq, \n          value: usersAndSessionsType,\n        }] : []),\n      ],\n    };\n  }\n\n  public static addDurationsOrPointsTypeFilter(\n    existingFilterDto: IFilterDto,\n    durationsOrPoints?: AccessCodeStatisticsDurationsOrPoints,\n    durationsOrPointsType?: AccessCodeStatisticsDurationsOrPointsType,\n  ): IFilterDto {\n    return {\n      ...existingFilterDto,\n      filter: [\n        ...(existingFilterDto.filter ? existingFilterDto.filter : []),\n        ...(durationsOrPoints ? [{ \n          field: 'durationsOrPoints' as VirtualFilterField, \n          type: FilterOperationType.Eq, \n          value: durationsOrPoints,\n        }] : []),\n        ...(durationsOrPointsType ? [{ \n          field: 'durationsOrPointsType' as VirtualFilterField, \n          type: FilterOperationType.Eq, \n          value: durationsOrPointsType,\n        }] : []),\n      ],\n    };\n  }\n\n  public static getFilterForQuery(\n    query: IAccessCodeStatisticsUsersAndSessionsQueryDto \n      | IAccessCodeStatisticsDurationsOrPointsQueryDto,\n  ): IFilterDto {\n    return {\n      filter: [\n        ...(query.filter.fromDate ? [{ \n          field: 'fromDate' as VirtualFilterField, \n          type: FilterOperationType.Gte, \n          value: query.filter.fromDate,\n        }] : []),\n        ...(query.filter.toDate ? [{ \n          field: 'toDate' as VirtualFilterField, \n          type: FilterOperationType.Lt, \n          value: query.filter.toDate,\n        }] : []),\n        ...(query.filter.intervalType ? [{ \n          field: 'intervalType' as VirtualFilterField, \n          type: FilterOperationType.Eq, \n          value: query.filter.intervalType,\n        }] : []),\n        ...(query.filter.courseIds ? [{ \n          field: 'courseIds' as VirtualFilterField, \n          type: FilterOperationType.In, \n          value: query.filter.courseIds,\n        }] : []),\n        ...(query.filter.tenantIds ? [{ \n          field: 'tenantIds' as VirtualFilterField, \n          type: FilterOperationType.In, \n          value: query.filter.tenantIds,\n        }] : []),\n        ...(query.filter.userIds ? [{ \n          field: 'userIds' as VirtualFilterField, \n          type: FilterOperationType.In, \n          value: query.filter.userIds,\n        }] : []),\n      ],\n    };\n  }\n}","import { IProgressData } from 'common/aurelia-components/statistics-chapter-progress/statistics-chapter-progress';\nimport { ICourseContentDto } from 'common/dto/course/content/course.interface';\nimport { IPageProgressReturnDto, IPageProgressWithDetailsReturnDto } from 'common/dto/course/progress/course.progress.interface';\nimport { QuizStatus } from 'common/dto/course/quiz/quiz.response';\nimport { PageType } from 'common/enum/course.enum';\nimport { ArrayUtils } from './ArrayUtils';\nimport { CourseUtils } from './CourseUtils';\nimport { MathUtils } from './MathUtils';\n\nexport class StatisticsUtils {\n  public static getPercentage(subSet: number, total: number): number {\n    const rawPercent = (subSet * 100) / total;\n    if(Number.isNaN(rawPercent) || !Number.isFinite(rawPercent)) return 0;\n    if(rawPercent > 100) return Math.floor(rawPercent);\n    return MathUtils.roundProgress(rawPercent);\n  }\n\n  public static getLearnProgressData(course: ICourseContentDto, pageProgresses: IPageProgressWithDetailsReturnDto[] | IPageProgressReturnDto[]): IProgressData[] {\n    let progressDataLearn: IProgressData[] = [];\n    const pageProgressesByPageId: Partial<Record<number, IPageProgressReturnDto>> = ArrayUtils.toRecord(pageProgresses, p => p.pageId, p => p);\n    for(const page of CourseUtils.getLearningRootPagesWithoutIntro(course)) {\n      const subPageIds = CourseUtils.getAllSubPages(course, page.id).map(p => p.id);\n\n      const pointsEarned = page.type === PageType.Page ? pageProgresses.find(p => p.pageId === page.id)?.awardedPoints ?? 0\n        : pageProgresses\n          .filter(pageProgress => subPageIds.includes(pageProgress.pageId))\n          .reduce((acc, pageProgress) => acc + pageProgress.awardedPoints, 0);\n\n      progressDataLearn.push({\n        chapterId: page.id,\n        chapterProgressPercentage: 0,\n        chapterTitle: page.title,\n        pointsEarned,\n      });\n\n      progressDataLearn = progressDataLearn.map(p => (p.chapterId === page.id ? {\n        ...p,\n        chapterProgressPercentage: pageProgressesByPageId[page.id]?.percentage ?? 0,\n      } : p));\n    }\n    return progressDataLearn;\n  }\n\n  public static getPercentageFromQuizStatus(exam: QuizStatus): number {\n    return 'pointsAvailable' in exam && exam.pointsAvailable && exam.pointsReached\n      ? Math.floor((exam.pointsReached * 100) / exam.pointsAvailable)\n      : 0;\n  }\n}\n","import { ValidationUtils } from './ValidationUtils';\r\n\r\ntype SorterFunction<T> = (a: T, b: T) => 0 | 1 | -1;\r\nexport class StringUtils {\r\n  public static createAlphabeticalSorter<T>(valueRetriever: (itm: T) => string | null): SorterFunction<T> {\r\n    return (a: T, b: T) => {\r\n      const aVal = valueRetriever(a);\r\n      const bVal = valueRetriever(b);\r\n\r\n      if(aVal == null || (bVal != null && aVal < bVal)) return -1;\r\n      if(bVal == null || aVal > bVal) return 1;\r\n      return 0;\r\n    };\r\n  }\r\n\r\n  public static replaceMinusWithSoftHyphen(text?: string | null): string {\r\n    return text?.replace(/-/g, String.fromCharCode(0xAD)) ?? '';\r\n  }\r\n\r\n  public static truncateWithEllipsis(text: string, maxLength: number): string {\r\n    const truncateStartPos = maxLength - 3;\r\n    return text.length > maxLength ? `${text.slice(0, truncateStartPos)}…` : text;\r\n  }\r\n\r\n  public static formatNumberGrouped(value: number): string {\r\n    return Intl.NumberFormat('de', {\r\n      useGrouping: true,\r\n    }).format(value);\r\n  }\r\n\r\n  public static formatFileSize(bytes: number): string {\r\n    const units = ['B', 'KB', 'MB', 'GB'];\r\n    const kilo = 1000;\r\n    let current = bytes;\r\n    let i = 0;\r\n    for(; ;) {\r\n      if(current < kilo || i >= units.length - 1) break;\r\n      current = Math.floor(current / kilo);\r\n      i++;\r\n    }\r\n\r\n    return `${current}${units[i]}`;\r\n  }\r\n\r\n  /**\r\n   * stolen from https://stackoverflow.com/a/6969486\r\n   * escapes the text so it doesn't contain any special regex characters\r\n   *\r\n   * @param {string} str\r\n   * @returns {string}\r\n   */\r\n  public static escapeStringForRegex(str: string): string {\r\n    return str.replace(/[.*+?^${}()|[\\]\\\\/]/g, '\\\\$&');\r\n  }\r\n\r\n  /**\r\n   * A Regex-Based implementation of replaceAll, since replaceAll doesn't exist in some older JS/Node versions\r\n   */\r\n  public static replaceAll(str: string, toReplace: string, replacement: string): string {\r\n    return str.replace(new RegExp(this.escapeStringForRegex(toReplace), 'gm'), replacement);\r\n  }\r\n\r\n  public static parsePhoneNumber(\r\n    phoneNumber: string,\r\n    allowedCallingCodes: string[],\r\n  ): {\r\n    callingCode: string | undefined;\r\n    nationalNumber: string | undefined;\r\n  } {\r\n    const [, callingCode, nationalNumber] = this.buildPhoneNumberRegex(allowedCallingCodes).exec(phoneNumber) ?? [];\r\n\r\n    return {\r\n      callingCode,\r\n      nationalNumber,\r\n    };\r\n  }\r\n\r\n  public static buildPhoneNumberRegex(allowedCallingCodes: string[]): RegExp {\r\n    const callingCodesToMatch = [...allowedCallingCodes]\r\n      .sort((a, b) => b.length - a.length) // sort longest codes to the front, so the longest match wins\r\n      .map(h => StringUtils.escapeStringForRegex(h)).join('|');\r\n    return new RegExp(`^\\\\+(${callingCodesToMatch})(.*)$`);\r\n  }\r\n\r\n  public static formatPhoneNumber(phoneNumberCc?: string | null, phoneNumberNn?: string | null): string {\r\n    return phoneNumberCc && phoneNumberNn ? `+${phoneNumberCc}${phoneNumberNn}` : '';\r\n  }\r\n\r\n  public static removeExtension(fileName?: string): string {\r\n    return fileName ? fileName.split('.').slice(0, -1).join('.') : '';\r\n  }\r\n\r\n  public static formatUrl(url: string): string {\r\n    return url.replace(/^https?:\\/\\//, '');\r\n  }\r\n\r\n  public static formatIdentifier(identifier: string): string {\r\n    let identifierFormatted = identifier.trim();\r\n    if(!ValidationUtils.EMAIL_REGEX.test(identifier)) {\r\n      const phoneNumber = identifier.trim();\r\n      identifierFormatted = /^00/.test(phoneNumber)\r\n        ? phoneNumber.replace(/^00/, '+')\r\n        : phoneNumber.replace(/^0+/, '+43');\r\n    }\r\n    return identifierFormatted;\r\n  }\r\n}\r\n","interface IOptions {\n  aligned: boolean;\n  immediate: boolean;\n}\n\nexport class TimeUtils {\n  public static getDurationFormatted(timeInSeconds: number): string {\n    const secondsRoundedToFullMinutes = Math.ceil(timeInSeconds / 60) * 60;\n    const days = Math.floor(secondsRoundedToFullMinutes / 86400);\n    const hours = Math.floor((secondsRoundedToFullMinutes - days * 86400) / 3600);\n    const minutes = Math.floor((secondsRoundedToFullMinutes - days * 86400 - hours * 3600) / 60);\n\n    const minutesDisplay = `${minutes}min`;\n    const hoursDisplay = hours ? `${hours}h` : '';\n    const daysDisplay = days ? `${days}d` : '';\n\n    return [daysDisplay, hoursDisplay, minutesDisplay].join(' ');\n  }\n\n  public static getAccurateInterval = (\n    callback: (scheduledTime: number) => void,\n    interval: number,\n    opts?: IOptions,\n  ): { clear: () => void } => {\n    const now = new Date().getTime();\n    let nextAt = now;\n    let timeout: number | null = null;\n\n    if(opts?.aligned) {\n      nextAt += interval - (now % interval);\n    }\n    if(!opts?.immediate) {\n      nextAt += interval;\n    }\n\n    const wrapper = (): void => {\n      const scheduledTime = nextAt;\n      nextAt += interval;\n      timeout = window.setTimeout(wrapper, nextAt - new Date().getTime());\n      callback(scheduledTime);\n    };\n\n    const clear = (): void => {\n      if(timeout)\n        return window.clearTimeout(timeout);\n    };\n\n    timeout = window.setTimeout(wrapper, nextAt - new Date().getTime());\n\n    return {\n      clear,\n    };\n  };\n}","import { IProgressData } from 'common/aurelia-components/statistics-chapter-progress/statistics-chapter-progress';\nimport { ANSWER_COUNT_REQUIRED_FOR_COMPLETION, DEFAULT_POINTS_PER_QUESTION } from 'common/constants/quiz.constants';\nimport { ICourseContentDto } from 'common/dto/course/content/course.interface';\nimport { IQuestionDto } from 'common/dto/course/content/question.interface';\nimport { ITrainingProgress, ITrainingProgressInfo } from 'common/interfaces/training.interface';\nimport { ArrayUtils } from './ArrayUtils';\nimport { CourseUtils } from './CourseUtils';\nimport { MathUtils } from './MathUtils';\n\nexport class TrainUtils {\n  private constructor() { }\n\n  public static getProgressInfo(progress: ITrainingProgress): ITrainingProgressInfo {\n    if(progress.answeredQuestions === 0) return { progress: 0, points: 0 };\n    const points = progress.answeredQuestions * DEFAULT_POINTS_PER_QUESTION;\n    const progressPercentage = TrainUtils.getTrainingProgressFromTrainingStats(progress);\n    return {\n      progress: MathUtils.roundProgress(progressPercentage, false),\n      points,\n    };\n  }\n\n  /**\n   * Calculates the training progress for each chapter and in total.\n   * For each such block T the progress is calculated as such:\n   *  - find the minimum answer count m within T\n   *  - for each question q within T calculate c = min(times q was correctly answered, m+1) / (cycles needed for 100%) (100% = 1)\n   *  - divide the sum of all c by the size of T (and multiply by 100 for a nice percent value)\n   */\n  public static getFreeTrainingProgress<T extends Date | string>(answerHistory: IAnswerHistory[], course: ICourseContentDto<T>, limitTo100Percent = false): TrainingProgressRecord {\n    const trainingQuestions = CourseUtils.getTrainingQuestions(course);\n    const chaptersByPageIds = CourseUtils.getRootChapterIdsByPageId(course);\n    const chapterIdByQuestionId = ArrayUtils.toRecord(trainingQuestions, q => q.id, q => q.pageId == null ? 'none' : chaptersByPageIds[q.pageId]);\n    const correctAnswerCountByQuestionId = ArrayUtils.toRecord<number, IAnswerHistory, number | undefined>(\n      answerHistory,\n      a => a.questionId,\n      (a, existing) => (existing ?? 0) + (a.correct ? 1 : 0),\n    );\n\n    // find the least answered question's answer count per chapter + overall\n    let totalMinAnswerCount: number | undefined;\n    const minAnswerCountByChapterId = ArrayUtils.toRecord<keyof TrainingProgressRecord, IQuestionDto, number | undefined>(\n      trainingQuestions,\n      q => chapterIdByQuestionId[q.id],\n      (q, existing) => {\n        const questionAnsweredCount = correctAnswerCountByQuestionId[q.id] ?? 0;\n\n        if(totalMinAnswerCount == null || questionAnsweredCount < totalMinAnswerCount) {\n          totalMinAnswerCount = questionAnsweredCount;\n        }\n\n        return existing == null || questionAnsweredCount < existing\n          ? questionAnsweredCount\n          : existing;\n      },\n    );\n\n    // calculate the training progress for each chapter + overall\n    const totalTrainingProgress: ITrainingProgress = {\n      answeredQuestions: 0,\n      questionCompleteness: 0,\n      totalQuestions: trainingQuestions.length,\n    };\n\n    const trainingProgressbyChapterId = ArrayUtils.toRecord<keyof TrainingProgressRecord, IQuestionDto, ITrainingProgress>(\n      trainingQuestions,\n      q => chapterIdByQuestionId[q.id],\n      (q, existing) => {\n        const questionAnsweredCount = correctAnswerCountByQuestionId[q.id] ?? 0;\n        totalTrainingProgress.answeredQuestions += questionAnsweredCount;\n        totalTrainingProgress.questionCompleteness += TrainUtils.getQuestionCompleteness(questionAnsweredCount, totalMinAnswerCount ?? 0, limitTo100Percent);\n\n        return {\n          answeredQuestions: (existing?.answeredQuestions ?? 0) + questionAnsweredCount,\n          totalQuestions: (existing?.totalQuestions ?? 0) + 1,\n          questionCompleteness: (existing?.questionCompleteness ?? 0) + TrainUtils.getQuestionCompleteness(\n            questionAnsweredCount,\n            minAnswerCountByChapterId[chapterIdByQuestionId[q.id]] ?? 0,\n            limitTo100Percent,\n          ),\n        };\n      },\n    );\n\n    trainingProgressbyChapterId.all = totalTrainingProgress;\n\n    return trainingProgressbyChapterId;\n  }\n\n  public static getTrainingMaxPoints(questionsTotal: number): number {\n    return questionsTotal * DEFAULT_POINTS_PER_QUESTION * ANSWER_COUNT_REQUIRED_FOR_COMPLETION;\n  }\n\n  public static getTrainingProgressFromTrainingStats(trainingStats: Partial<ITrainingProgress>): number {\n    const trainStats = {\n      totalQuestions: trainingStats?.totalQuestions ?? 0,\n      questionCompleteness: trainingStats?.questionCompleteness ?? 0,\n    };\n    return trainStats.totalQuestions > 0\n      ? (trainStats.questionCompleteness * 100 / (trainStats.totalQuestions ?? 0))\n      : 0;\n  }\n\n  public static getChapterProgresses(\n    course: ICourseContentDto,\n    dataRecords: Record<number, ITrainingProgress | undefined>,\n  ): Array<IProgressData> {\n    return CourseUtils.getTrainingRootPages(course)\n      // filter out chapters that have no questions\n      .filter(p => !!dataRecords[p.id]?.totalQuestions)\n      .map(p => {\n        const pageProgress = dataRecords[p.id] ?? {\n          answeredQuestions: 0,\n          totalQuestions: 0,\n          questionCompleteness: 0,\n        };\n\n        return {\n          chapterId: p.id,\n          chapterTitle: p.title,\n          chapterProgressPercentage: TrainUtils.getTrainingProgressFromTrainingStats(pageProgress),\n          pointsEarned: pageProgress.answeredQuestions * DEFAULT_POINTS_PER_QUESTION,\n        };\n      });\n  }\n\n  public static createDifficultyOptions(baseOptions: IDifficultyOption[], availableDifficulties: number[]): IDifficultyOptionState[] {\n    const availableDifficultySet = new Set(availableDifficulties);\n    return baseOptions.map(option => ({\n      ...option,\n      isSelected: availableDifficultySet.has(option.value),\n      disabled: !availableDifficultySet.has(option.value),\n    }));\n  }\n\n  public static getFsDifficulties(): IDifficultyOption[] {\n    return [\n      {\n        labelTk: 'resources.elements.training-configurator-dialog.difficulty-easy',\n        value: 1,\n      },\n      {\n        labelTk: 'resources.elements.training-configurator-dialog.difficulty-medium',\n        value: 3,\n      },\n      {\n        labelTk: 'resources.elements.training-configurator-dialog.difficulty-hard',\n        value: 5,\n      },\n    ];\n  }\n\n  /**\n   * @param questionAnsweredCount number of times this question has been answered correctly\n   * @param minAnsweredCount number of times the least answered related question has been answered correctly\n   * @param limitTo100Percent if true, the output is capped at 1 (100%)\n   * @returns the \"completeness\" value for this question, with 1 = 100%\n   */\n  private static getQuestionCompleteness(questionAnsweredCount: number, minAnsweredCount: number, limitTo100Percent: boolean): number {\n    // count all answers until 100%, but after 100% count at most one more than the least answered question, to prevent exploits from answering only a single question over and over\n    const maxCountedAnswers = Math.max(ANSWER_COUNT_REQUIRED_FOR_COMPLETION, minAnsweredCount + 1);\n    const countedAnswers = Math.min(questionAnsweredCount, maxCountedAnswers);\n\n    const completeness = countedAnswers / ANSWER_COUNT_REQUIRED_FOR_COMPLETION;\n\n    return limitTo100Percent\n      ? Math.min(1, completeness)\n      : completeness;\n  }\n}\n\nexport interface IAnswerHistory {\n  questionId: number;\n  correct: boolean;\n}\n\nexport type TrainingProgressRecord = Record<number | 'all' | 'none', ITrainingProgress>;\n\n\nexport interface IDifficultyOption {\n  labelTk: string;\n  value: number;\n}\n\nexport interface IDifficultyOptionState extends IDifficultyOption {\n  isSelected: boolean;\n  disabled: boolean;\n}\n","import { MIN_DIGITS_IN_PHONE_NUMBER, STUDENT_MAX_AGE_IN_YEARS, STUDENT_MIN_AGE_IN_YEARS } from 'common/constants/auth.constants';\nimport { StatesOfAustria } from 'common/enum/states.enum';\nimport Moment from 'moment';\nimport { StringUtils } from './StringUtils';\n\nexport class ValidationUtils {\n  public static readonly EMAIL_REGEX = /\\S+@\\S+\\.\\S+/;\n  private static readonly PHONE_REGEX = /\\D/g;\n\n  public static isBirthdateValid(birthdate: string | undefined | null): string | null {\n    return !birthdate || !Moment(birthdate).isValid() ? 'common.validationUtils.dateInvalidError' : null;\n  }\n\n  public static isBirthdateTodayOrInFuture(birthdate: string | undefined | null): string | null {\n    return (Moment(birthdate).isSame(Moment(), 'day') || Moment(birthdate).isAfter()) ? 'common.validationUtils.dateInFutureError' : null;\n  }\n\n  public static isBirthdateFarEnoughInPast(birthdate: string | undefined | null): string | null {\n    return Moment(birthdate).isAfter(Moment().subtract(STUDENT_MIN_AGE_IN_YEARS, 'years')) ? 'common.validationUtils.minimumAgeError' : null;\n  }\n\n  public static isBirthdateTooFarInPast(birthdate: string | undefined | null): string | null {\n    return Moment(birthdate).isBefore(Moment().subtract(STUDENT_MAX_AGE_IN_YEARS, 'years')) ? 'common.validationUtils.dateInPastError' : null;\n  }\n\n  public static checkFirstname(firstname: string | null | undefined): string | null {\n    return !firstname ? 'common.validationUtils.noFirstnameError' : null;\n  }\n\n  public static checkLastname(lastname: string | null | undefined): string | null {\n    return !lastname ? 'common.validationUtils.noLastnameError' : null;\n  }\n\n  public static checkState(state: string | null | undefined): string | null {\n    return !(Object.values(StatesOfAustria).includes(state as StatesOfAustria)) ? 'common.validationUtils.stateInvalidError' : null;\n  }\n\n  public static checkOptionalEmail(email: string | null): string | null {\n    return (email && !this.EMAIL_REGEX.test(email)) ? 'common.validationUtils.emailInvalidError' : null;\n  }\n\n  public static checkMandatoryEmail(email: string | null): string | null {\n    return (!email || !this.EMAIL_REGEX.test(email)) ? 'common.validationUtils.emailInvalidError' : null;\n  }\n\n  public static checkMinAssignedStates(assignedStates: string[] | null): string | null {\n    return !assignedStates?.length ? 'common.validationUtils.minAssignedStatesError' : null;\n  }\n\n  public static checkPositiveNumber(testNumber: number | null): string | null {\n    return !testNumber || testNumber <= 0 ? 'common.validationUtils.positiveNumberError' : null;\n  }\n\n  public static checkMandatoryPhoneNumber(phoneNumber: string | null | undefined, allowedCallingCodes?: string[]): string | null {\n    if(!phoneNumber) return 'common.validationUtils.phoneNumberInvalidError';\n\n    if(allowedCallingCodes) {\n      const { nationalNumber } = StringUtils.parsePhoneNumber(phoneNumber, allowedCallingCodes);\n      if(nationalNumber?.replace(this.PHONE_REGEX, '').startsWith('0')) {\n        return 'common.validationUtils.nationalNumberMustNotStartWithZeroError';\n      }\n    }\n\n    const phoneNumberDigits = phoneNumber.replace(this.PHONE_REGEX, '');\n    if(phoneNumberDigits.length < MIN_DIGITS_IN_PHONE_NUMBER) {\n      return 'common.validationUtils.phoneNumberInvalidError';\n    }\n\n    return null;\n  }\n\n  public static checkOptionalPhoneNumber(phoneNumber: string | null | undefined, allowedCallingCodes?: string[]): string | null {\n    return phoneNumber ? this.checkMandatoryPhoneNumber(phoneNumber, allowedCallingCodes) : null;\n  }\n\n  public static checkEmailOrPhoneNumber(emailOrPhoneNumber: string, {\n    phoneNumberEnabled,\n    allowedCallingCodes,\n  }: {\n    phoneNumberEnabled: boolean;\n    allowedCallingCodes?: string[];\n  }): string | null {\n    if(!phoneNumberEnabled) {\n      return this.checkMandatoryEmail(emailOrPhoneNumber);\n    }\n\n    if(this.checkMandatoryEmail(emailOrPhoneNumber) == null) {\n      return null;\n    }\n\n    if(emailOrPhoneNumber.includes('@') || this.checkOptionalPhoneNumber(emailOrPhoneNumber, allowedCallingCodes) != null) {\n      return 'common.validationUtils.emailOrPhoneNumberInvalidError';\n    }\n\n    return null;\n  }\n\n  public static checkCustom(errorMsgTk: string, value?: string | null): string | null {\n    return !value ? errorMsgTk : null;\n  }\n}\n","import { Container } from 'aurelia-framework';\nimport { VimeoApiService } from 'common/aurelia-services/VimeoApiService';\nimport { UniversalBlock } from 'common/dto/content-editor/content.editor.interface';\nimport { BlockType } from 'common/dto/course/content/course.types';\nimport { BunnyUtils } from './BunnyUtils';\n\n/**\n * utils for all sorts of video blocks (course, ias, presentation)\n */\nexport class VideoBlockUtils {\n  private constructor() { }\n\n  public static fetchVideoThumbnail(block: UniversalBlock): Promise<string | null> {\n    return this.fetchVideoThumbnailInternal(block, false);\n  }\n\n  public static fetchPipVideoThumbnail(block: UniversalBlock): Promise<string | null> {\n    return this.fetchVideoThumbnailInternal(block, true);\n  }\n\n  private static async fetchVideoThumbnailInternal(block: UniversalBlock, pip: boolean): Promise<string | null> {\n    const { type } = block;\n    const videoUrl = pip ? block.pipVideoUrl : block.videoUrl;\n    const vimeoId = pip ? block.pipVimeoId : block.vimeoId;\n    const bunnyId = pip ? block.pipBunnyId : block.bunnyId;\n\n    try {\n      switch(type) {\n        case BlockType.YOUTUBE: {\n          // ID extraction taken from private plyr functions\n          const videoId = videoUrl?.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?.[2];\n          return videoId ? `http://img.youtube.com/vi/${videoId}/0.jpg` : null;\n        }\n        case BlockType.VIMEO: {\n          // ID extraction taken from private plyr functions\n          const videoId = Number(videoUrl?.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?.[2]);\n          return Number.isNaN(videoId) ? null : await this.fetchVimeoThumbnail(videoId);\n        }\n        default: {\n          if(vimeoId) {\n            return await this.fetchVimeoThumbnail(vimeoId);\n          } else if(bunnyId && videoUrl) {\n            return BunnyUtils.thumbnailFromUrl(videoUrl);\n          }\n          throw new Error('vimeoId or bunnyId must be defined');\n        }\n      }\n    } catch(ex) {\n      console.error(ex);\n    }\n\n    return null;\n  }\n\n  private static async fetchVimeoThumbnail(vimeoId: number): Promise<string | null> {\n    if(!Container.instance.hasResolver(VimeoApiService)) {\n      Container.instance.autoRegister(VimeoApiService);\n    }\n\n    const api = Container.instance.get(VimeoApiService);\n    const videoInfo = await api.getVideoInfo(vimeoId);\n    return videoInfo?.thumbnail_url ?? null;\n  }\n}\n","export function assertNotNullOrUndefined<T>(value: T | null | undefined, errorMessage: string): asserts value is T {\n  if(value === null || value === undefined) {\n    throw new Error(errorMessage);\n  }\n}\n\nexport function assertEqual<TValue, TExpected extends TValue>(value: TValue, expected: TExpected, errorMessage: string): asserts value is TExpected {\n  if(value !== expected) {\n    throw new Error(errorMessage);\n  }\n}\n","export type OmitStrict<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;\nexport type Optional<T, K extends keyof T> = Omit<T, K> & Pick<Partial<T>, K>;\n\n/**\n * Returns a type with only keys whose value matches TValue\n * \n * @example\n * ```\n * PickByType<{ id: number; name: string; description: string; render: () => void }, string>\n * => { name: string; description: string }\n * ```\n */\nexport type PickByType<T, TValue> = {\n  [K in keyof T as T[K] extends TValue ? K : never]: T[K];\n};\n\nexport function isNotNullish<T>(thing?: T | null): thing is T {\n  return thing != null;\n}\n"],"names":[],"sourceRoot":""}