515 lines
12 KiB
JavaScript
515 lines
12 KiB
JavaScript
(() => {
|
|
function normalizeText(value) {
|
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function cleanString(value) {
|
|
return normalizeText(value);
|
|
}
|
|
|
|
function firstNonEmptyString(values) {
|
|
for (const value of values) {
|
|
const normalized = cleanString(value);
|
|
|
|
if (normalized) {
|
|
return normalized;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function normalizeString(value) {
|
|
return normalizeText(value).toUpperCase();
|
|
}
|
|
|
|
function toNumber(value) {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
|
|
const numeric = Number.parseFloat(String(value ?? "").replace(/,/g, ""));
|
|
return Number.isFinite(numeric) ? numeric : 0;
|
|
}
|
|
|
|
function toIsoDateString(value) {
|
|
const normalized = cleanString(value);
|
|
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
const date = new Date(`${normalized}T12:00:00.000Z`);
|
|
|
|
return Number.isNaN(date.getTime()) ? normalized : date.toISOString();
|
|
}
|
|
|
|
function extractArray(value, candidateKeys = []) {
|
|
if (Array.isArray(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (!value || typeof value !== "object") {
|
|
return [];
|
|
}
|
|
|
|
for (const key of candidateKeys) {
|
|
if (Array.isArray(value[key])) {
|
|
return value[key];
|
|
}
|
|
}
|
|
|
|
for (const nestedValue of Object.values(value)) {
|
|
if (Array.isArray(nestedValue)) {
|
|
return nestedValue;
|
|
}
|
|
|
|
if (nestedValue && typeof nestedValue === "object") {
|
|
const nestedArray = extractArray(nestedValue, candidateKeys);
|
|
|
|
if (nestedArray.length > 0) {
|
|
return nestedArray;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
const results = new Array(items.length);
|
|
let nextIndex = 0;
|
|
|
|
async function worker() {
|
|
while (nextIndex < items.length) {
|
|
const currentIndex = nextIndex;
|
|
nextIndex += 1;
|
|
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
}
|
|
}
|
|
|
|
const workerCount = Math.min(concurrency, items.length || 1);
|
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
|
|
return results;
|
|
}
|
|
|
|
function getFieldValue(source, paths) {
|
|
if (!source || typeof source !== "object") {
|
|
return "";
|
|
}
|
|
|
|
for (const path of paths) {
|
|
if (Object.prototype.hasOwnProperty.call(source, path)) {
|
|
return source[path];
|
|
}
|
|
|
|
const parts = String(path).split(".");
|
|
let current = source;
|
|
|
|
for (const part of parts) {
|
|
if (!current || typeof current !== "object") {
|
|
current = "";
|
|
break;
|
|
}
|
|
|
|
current = current[part];
|
|
}
|
|
|
|
if (current !== undefined && current !== null && current !== "") {
|
|
return current;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function flattenDisplayRecord(value, prefix = "", output = []) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
if (prefix) {
|
|
output.push([prefix, value]);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
for (const [key, nestedValue] of Object.entries(value)) {
|
|
const nextKey = prefix ? `${prefix}.${key}` : key;
|
|
|
|
if (
|
|
nestedValue &&
|
|
typeof nestedValue === "object" &&
|
|
!Array.isArray(nestedValue)
|
|
) {
|
|
flattenDisplayRecord(nestedValue, nextKey, output);
|
|
} else {
|
|
output.push([nextKey, nestedValue]);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function isVisible(element) {
|
|
const style = window.getComputedStyle(element);
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
return (
|
|
style.display !== "none" &&
|
|
style.visibility !== "hidden" &&
|
|
Number.parseFloat(style.opacity || "1") > 0 &&
|
|
rect.width > 0 &&
|
|
rect.height > 0
|
|
);
|
|
}
|
|
|
|
function formatWholeNumber(value) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
}
|
|
|
|
function formatCurrency(value) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency: "USD",
|
|
maximumFractionDigits: 0,
|
|
}).format(value || 0);
|
|
}
|
|
|
|
function formatHours(value) {
|
|
return new Intl.NumberFormat("pt-BR", {
|
|
maximumFractionDigits: 2,
|
|
}).format(toNumber(value));
|
|
}
|
|
|
|
function formatWeekNumber(value) {
|
|
return String(toNumber(value)).padStart(2, "0");
|
|
}
|
|
|
|
function formatOperationalWeekLabel(weekInfo) {
|
|
if (!weekInfo) {
|
|
return "";
|
|
}
|
|
|
|
return `FY${formatWeekNumber(weekInfo.fiscalYear % 100)}-Q${
|
|
weekInfo.quarter
|
|
}-W${formatWeekNumber(weekInfo.weekNumber)}`;
|
|
}
|
|
|
|
function formatPercent(value) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
maximumFractionDigits: 1,
|
|
}).format(value || 0) + "%";
|
|
}
|
|
|
|
function formatProgressPercent(value) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
maximumFractionDigits: 0,
|
|
}).format(value || 0) + "%";
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return String(value);
|
|
}
|
|
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
dateStyle: "medium",
|
|
}).format(date);
|
|
}
|
|
|
|
function getDateInputValue(value) {
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return "";
|
|
}
|
|
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function formatDateInputDisplay(value) {
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return cleanString(value);
|
|
}
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = date.getFullYear();
|
|
|
|
return `${day}/${month}/${year}`;
|
|
}
|
|
|
|
function formatDateForTimeEntryRequest(value) {
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return "";
|
|
}
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = new Intl.DateTimeFormat("en-US", { month: "short" })
|
|
.format(date)
|
|
.toUpperCase();
|
|
const year = date.getFullYear();
|
|
|
|
return `${day}-${month}-${year}`;
|
|
}
|
|
|
|
function parseTimeEntryDate(value) {
|
|
const text = cleanString(value);
|
|
const match = text.match(/^(\d{1,2})-([A-Z]{3})-(\d{4})$/i);
|
|
|
|
if (!match) {
|
|
return parseDatePreservingDateOnly(text);
|
|
}
|
|
|
|
const monthIndex = [
|
|
"JAN",
|
|
"FEB",
|
|
"MAR",
|
|
"APR",
|
|
"MAY",
|
|
"JUN",
|
|
"JUL",
|
|
"AUG",
|
|
"SEP",
|
|
"OCT",
|
|
"NOV",
|
|
"DEC",
|
|
].indexOf(match[2].toUpperCase());
|
|
|
|
if (monthIndex < 0) {
|
|
return new Date(Number.NaN);
|
|
}
|
|
|
|
return new Date(Number(match[3]), monthIndex, Number(match[1]));
|
|
}
|
|
|
|
function formatDateTime(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
|
|
const date = new Date(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return String(value);
|
|
}
|
|
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
}).format(date);
|
|
}
|
|
|
|
function getLocalDateKey(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return "";
|
|
}
|
|
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function getOperationalWeekInfo(value) {
|
|
const date = parseDatePreservingDateOnly(value);
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
const operationalYearStart = new Date(date.getFullYear(), 5, 1);
|
|
|
|
if (date < operationalYearStart) {
|
|
operationalYearStart.setFullYear(operationalYearStart.getFullYear() - 1);
|
|
}
|
|
|
|
const normalizedDate = new Date(
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate()
|
|
);
|
|
const normalizedStart = new Date(
|
|
operationalYearStart.getFullYear(),
|
|
operationalYearStart.getMonth(),
|
|
operationalYearStart.getDate()
|
|
);
|
|
const daysSinceStart = Math.floor(
|
|
(normalizedDate.getTime() - normalizedStart.getTime()) / 86400000
|
|
);
|
|
const monthsSinceStart =
|
|
(normalizedDate.getFullYear() - normalizedStart.getFullYear()) * 12 +
|
|
normalizedDate.getMonth() -
|
|
normalizedStart.getMonth();
|
|
|
|
return {
|
|
fiscalYear: operationalYearStart.getFullYear() + 1,
|
|
quarter: Math.floor(monthsSinceStart / 3) + 1,
|
|
weekNumber: Math.floor(daysSinceStart / 7) + 1,
|
|
};
|
|
}
|
|
|
|
function getCalendarRowWeekDate(rowStartDate) {
|
|
const weekDate = new Date(rowStartDate);
|
|
|
|
if (weekDate.getDay() === 0) {
|
|
weekDate.setDate(weekDate.getDate() + 1);
|
|
}
|
|
|
|
return weekDate;
|
|
}
|
|
|
|
function parseDatePreservingDateOnly(value) {
|
|
if (value instanceof Date) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
const dateOnlyMatch = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
|
|
if (dateOnlyMatch) {
|
|
return new Date(
|
|
Number(dateOnlyMatch[1]),
|
|
Number(dateOnlyMatch[2]) - 1,
|
|
Number(dateOnlyMatch[3])
|
|
);
|
|
}
|
|
}
|
|
|
|
return new Date(value);
|
|
}
|
|
|
|
function decodeJwtPayload(token) {
|
|
const normalizedToken = cleanString(token);
|
|
|
|
if (
|
|
!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
|
|
normalizedToken
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const payloadPart = normalizedToken.split(".")[1];
|
|
const base64 = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
|
|
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
const json = decodeURIComponent(
|
|
Array.from(atob(paddedBase64), (char) => {
|
|
return `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
}).join("")
|
|
);
|
|
|
|
return JSON.parse(json);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getJwtExpiresAt(token) {
|
|
const payload = decodeJwtPayload(token);
|
|
const expiresAtSeconds = Number(payload?.exp);
|
|
|
|
return Number.isFinite(expiresAtSeconds) && expiresAtSeconds > 0
|
|
? expiresAtSeconds * 1000
|
|
: 0;
|
|
}
|
|
|
|
function isJwtExpired(token, skewSeconds = 60) {
|
|
const expiresAt = getJwtExpiresAt(token);
|
|
|
|
if (!expiresAt) {
|
|
return false;
|
|
}
|
|
|
|
const skewMilliseconds = Math.max(Number(skewSeconds) || 0, 0) * 1000;
|
|
|
|
return expiresAt <= Date.now() + skewMilliseconds;
|
|
}
|
|
|
|
function getErrorMessage(error) {
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
|
|
return "Unexpected error while fetching live data.";
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function cssEscape(value) {
|
|
if (window.CSS?.escape) {
|
|
return window.CSS.escape(String(value || ""));
|
|
}
|
|
|
|
return String(value || "").replace(/["\\]/g, "\\$&");
|
|
}
|
|
|
|
globalThis.ArchPanelUtils = Object.freeze({
|
|
normalizeText,
|
|
cleanString,
|
|
firstNonEmptyString,
|
|
normalizeString,
|
|
toNumber,
|
|
toIsoDateString,
|
|
extractArray,
|
|
mapWithConcurrency,
|
|
getFieldValue,
|
|
flattenDisplayRecord,
|
|
isVisible,
|
|
formatWholeNumber,
|
|
formatCurrency,
|
|
formatHours,
|
|
formatWeekNumber,
|
|
formatOperationalWeekLabel,
|
|
formatPercent,
|
|
formatProgressPercent,
|
|
formatDate,
|
|
getDateInputValue,
|
|
formatDateInputDisplay,
|
|
formatDateForTimeEntryRequest,
|
|
parseTimeEntryDate,
|
|
formatDateTime,
|
|
getLocalDateKey,
|
|
getOperationalWeekInfo,
|
|
getCalendarRowWeekDate,
|
|
parseDatePreservingDateOnly,
|
|
decodeJwtPayload,
|
|
getJwtExpiresAt,
|
|
isJwtExpired,
|
|
getErrorMessage,
|
|
escapeHtml,
|
|
cssEscape,
|
|
});
|
|
})();
|