let PreParedReportDataObject = null; ////////////////////////////////// // Pre render function // async function ayPreRender(ayData) { if (typeof ayPrepareData === "function") { PreParedReportDataObject = await ayPrepareData(ayData); } else { PreParedReportDataObject = ayData; } return true; } //##################################### HANDLEBARS HELPERS ################################################### /////////////////////////////////////// // Set our stock handlebars helpers // function ayRegisterHelpers() { Handlebars.registerHelper("ayCaps", function (ayValue) { return ayValue.toUpperCase(); }); Handlebars.registerHelper("ayDateTime", function (ayValue) { return utcDateToShortDateAndTimeLocalized(ayValue); }); Handlebars.registerHelper("ayDate", function (ayValue) { return utcDateToShortDateLocalized(ayValue); }); Handlebars.registerHelper("ayTime", function (ayValue) { return utcDateToShortTimeLocalized(ayValue); }); Handlebars.registerHelper("ayDecimal", function (ayValue) { return decimalLocalized(ayValue); }); Handlebars.registerHelper("ayCurrency", function (ayValue) { return currencyLocalized(ayValue); }); Handlebars.registerHelper("ayWiki", function (ayValue) { if (ayValue == null) { return ""; } //replace attachment urls with tokenized local urls let src = ayValue.replace(/\[ATTACH:(.*)\]/g, function (match, p1) { return attachmentDownloadUrl(p1); }); return new Handlebars.SafeString( DOMPurify.sanitize(marked.parse(src, { breaks: true })) ); }); Handlebars.registerHelper("ayJSON", function (obj) { return JSON.stringify(obj, null, 3); }); Handlebars.registerHelper("ayLink", function (text, url) { var url = Handlebars.escapeExpression(url), text = Handlebars.escapeExpression(text); return new Handlebars.SafeString( "" + text + "" ); }); Handlebars.registerHelper("ayLogo", function (size) { if (AYMETA.ayServerMetaData) { switch (size) { case "small": if (!AYMETA.ayServerMetaData.HasSmallLogo) { return ""; } break; case "medium": if (!AYMETA.ayServerMetaData.HasMediumLogo) { return ""; } break; case "large": if (!AYMETA.ayServerMetaData.HasLargeLogo) { return ""; } break; } } var url = `${Handlebars.escapeExpression( AYMETA.ayServerMetaData.ayApiUrl )}logo/${size}`; return new Handlebars.SafeString(""); }); Handlebars.registerHelper("ayT", function (translationKey) { if (ayTranslationKeyCache[translationKey] == undefined) { return `**Error: "${translationKey}" translation key not cached or unknown**`; // throw `ayT reporting helper error: the key "${translationKey}" is not present in the translation cache, did you forget to include it in your call to "await ayGetTranslations(['ExampleTranslationKey1','ExampleTranslationKey2','etc']);" in ayPrepareData()\nTranslationKeyCache contains: ${JSON.stringify( // ayTranslationKeyCache, // null, // 3 // )}?`; // return translationKey; } return ayTranslationKeyCache[translationKey]; }); /////////////////////////////////////////// // BarCode helper using // https://github.com/metafloor/bwip-js#browser-usage // Handlebars.registerHelper("ayBC", function (text, options) { let canvas = document.getElementById("aybarcode"); if (canvas == null) { canvas = document.createElement("canvas"); canvas.id = "aybarcode"; } let opt = JSON.parse(options); if (text == null) { text = ""; } else { text = text.toString(); } opt.text = text; opt.textxalign = opt.textxalign || "center"; bwipjs.toCanvas(canvas, opt); var url = canvas.toDataURL("image/png"); return new Handlebars.SafeString(""); }); } //eof /////////////////////////////////////////// // Concat helper using // https://stackoverflow.com/a/52571635/8939 // Handlebars.registerHelper("ayConcat", function () { arguments = [...arguments].slice(0, -1); return arguments.join(""); }); //##################################### LOCALIZATION & TRANSLATION ################################################### /////////////////////////////////////////// // Turn a utc date into a displayable // short date and time // function utcDateToShortDateAndTimeLocalized(ayValue) { if (!ayValue) { return ""; } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") let parsedDate = new Date(ayValue); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleString( AYMETA.ayClientMetaData.LanguageName || "en-US", { timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg", dateStyle: "short", timeStyle: "short", hour12: AYMETA.ayClientMetaData.Hour12 } ); } /////////////////////////////////////////// // Turn a utc date into a displayable // short date function utcDateToShortDateLocalized(ayValue) { if (!ayValue) { return ""; } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") let parsedDate = new Date(ayValue); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleDateString( AYMETA.ayClientMetaData.LanguageName || "en-US", { timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg", dateStyle: "short" } ); } /////////////////////////////////////////// // Turn a utc date into a displayable // short time function utcDateToShortTimeLocalized(ayValue) { if (!ayValue) { return ""; } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") let parsedDate = new Date(ayValue); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleTimeString( AYMETA.ayClientMetaData.LanguageName || "en-US", { timeZone: AYMETA.ayClientMetaData.TimeZoneName || "America/Winnipeg", timeStyle: "short", hour12: AYMETA.ayClientMetaData.Hour12 } ); } /////////////////////////////////////////// // CURRENCY LOCALIZATION // function currencyLocalized(ayValue) { if (!ayValue) { return ""; } return new Intl.NumberFormat( AYMETA.ayClientMetaData.LanguageName || "en-US", { style: "currency", currency: AYMETA.ayClientMetaData.CurrencyName || "USD" } ).format(ayValue); } /////////////////////////////////////////// // DECIMAL LOCALIZATION // function decimalLocalized(ayValue) { if (!ayValue) { return ""; } return new Intl.NumberFormat( AYMETA.ayClientMetaData.LanguageName || "en-US" ).format(ayValue); } ////////////////////////////////// // cache to hold translations keys // var ayTranslationKeyCache = {}; /////////////////////////////////// // GET TRANSLATIONS FROM API SERVER // async function ayGetTranslations(keys) { if (!keys || keys.length == 0) { return; } try { let transData = await ayPostToAPI("translation/subset", keys); transData.data.forEach(function storeFetchedTranslationItemsInCache( item ) { ayTranslationKeyCache[item.key] = item.value; }); } catch (error) { throw error; } } //##################################### API UTILITIES ################################################### /////////////////////////////////// // GET DATA FROM API SERVER // async function ayGetFromAPI(route, token) { token = token || AYMETA.ayClientMetaData.Authorization; if (route && !route.startsWith("http")) { route = AYMETA.ayServerMetaData.ayApiUrl + route; } try { let r = await fetch(route, { method: "get", mode: "cors", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: token } }); return await extractBodyEx(r); } catch (error) { throw error; } } /////////////////////////////////// // POST DATA TO API SERVER // async function ayPostToAPI(route, data, token) { token = token || AYMETA.ayClientMetaData.Authorization; if (route && !route.startsWith("http")) { route = AYMETA.ayServerMetaData.ayApiUrl + route; } //api expects custom fields to be a string not an object if (data && data.CustomFields && data.CustomFields.constructor === Object) { data.CustomFields = JSON.stringify(data.CustomFields); } try { fetchOptions = { method: "post", mode: "cors", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: token }, body: JSON.stringify(data) }; let r = await fetch(route, fetchOptions); return await extractBodyEx(r); } catch (error) { throw error; } } /////////////////////////////////// // PUT DATA TO API SERVER // async function ayPutToAPI(route, data, token) { token = token || AYMETA.ayClientMetaData.Authorization; if (route && !route.startsWith("http")) { route = AYMETA.ayServerMetaData.ayApiUrl + route; } //api expects custom fields to be a string not an object if (data && data.CustomFields && data.CustomFields.constructor === Object) { data.CustomFields = JSON.stringify(data.CustomFields); } try { fetchOptions = { method: "put", mode: "cors", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: token }, body: JSON.stringify(data) }; let r = await fetch(route, fetchOptions); return await extractBodyEx(r); } catch (error) { throw error; } } ///////////////////////////// // attachment download URL // (INTERNAL USE NOT DOCUMENTED FOR HELPER USE) function attachmentDownloadUrl(fileId, ctype) { let url = "attachment/download/" + fileId + "?t=" + AYMETA.ayClientMetaData.DownloadToken; if (ctype && ctype.includes("image")) { url += "&i=1"; } return AYMETA.ayServerMetaData.ayApiUrl + url; } //##################################### CODE UTILITIES ################################################### async function extractBodyEx(response) { if (response.status == 204) { //no content, nothing to process return response; } if (response.status == 202) { //Accepted, nothing to process return response; } const contentType = response.headers.get("content-type"); if (!contentType) { return response; } if (contentType.includes("json")) { return await response.json(); } if (contentType.includes("text/plain")) { return await response.text(); } if (contentType.includes("application/pdf")) { return await response.blob(); } return response; } ///////////////////////////////////////////////////////// // Group by function // reshapes input array into a new array grouped with // a key named "group" with that group's key value // and a key named "items" to contain all items // for that group and also a "count" of items added // // function ayGroupByKey(reportDataArray, groupByKeyName) { //array to hold grouped data const ret = []; //iterate through the raw reprot data for (let i = 0; i < reportDataArray.length; i++) { //search the ret array for a group with this name and if found return a reference to that group object let groupObject = ret.find( (z) => z.group == reportDataArray[i][groupByKeyName] ); if (groupObject != undefined) { //there is already a matching group in the return array so just push this raw report data record into it groupObject.items.push(reportDataArray[i]); //update the count for this group's items groupObject.count++; } else { //No group yet, so start a new one in the ret array and push this raw report data record ret.push({ group: reportDataArray[i][groupByKeyName], items: [reportDataArray[i]], count: 1 }); } } return ret; }