Files
Aislo/tools/stmate-test/report_link.js
T

526 lines
21 KiB
JavaScript

/* -*- coding: utf-8 -*- */
/**
* 보고서 시트 잇기 (report_link.js) — 원본 STMATE 엑셀의 시트 간 참조 수식 · 바로가기(→) · 시트 이름표 수식
* 근거: 지식DB STmate/old/20_분석 24 §3(ST_P_XLS Prn_FMLA_INFO) · 19 §3(목차·안내 시트) ·
* 원본 출력 엑셀 5건 수식 실측(봉화·영월·대흥·울진·영덕 — openpyxl 수식 읽기)
* 정답 대조: resources/tester/test_stmate_report_link.py(원본 수식 글 그대로)
* 시트 모형 행에 붙이는 것:
* _f {열: 값} — 수식 넣기 때만 쓰는 칸(수식 · 숨은 열쇠 글) · 값 칸은 계산 엔진 값 그대로
* _links {열: '시트!칸'} — 값 모드 바로가기(하이퍼링크)
*/
const StmateReportLink = (() => {
const TOC = "〓 목 차 〓";
const GUIDE = "※※안내※※";
const INIT = "〓 INITIAL 〓";
const FILE_NAME =
'=MID(CELL("filename",$A$1),FIND("]",CELL("filename",$A$1))+1,LEN(CELL("filename",$A$1)))';
// 시트 이름 — 한글·영문·숫자만이면 따옴표 없이(원본 꼴)
const q = (n) =>
/^[가-힣A-Za-z_][가-힣A-Za-z0-9_]*$/.test(n) ? n : `'${n.replace(/'/g, "''")}'`;
const colNum = (c) => c.split("").reduce((a, ch) => a * 26 + ch.charCodeAt(0) - 64, 0);
const colName = (n) =>
(n > 26 ? String.fromCharCode(64 + Math.floor((n - 1) / 26)) : "") +
String.fromCharCode(65 + ((n - 1) % 26));
const isCol = (k) => /^[A-Z]{1,2}$/.test(k);
// 시트 이름표 칸 — 2행에서 시트 이름이 적힌 열
const tagOf = (sh) =>
sh.rows[1] ? Object.keys(sh.rows[1]).find((k) => isCol(k) && sh.rows[1][k] === sh.name) : null;
// 목차 바로가기 열 — 숨은 도움 열이 있는 시트는 그 뒤(원본 실측) · 그 밖 이름표 다음 열
const LINK_COL = {
총괄내역서: "Q",
설계내역서: "AC",
일위대가표: "Z",
중기사용료: "Z",
단가산출근거: "Y",
};
const NO_LINK = new Set([TOC, GUIDE, INIT, "환율및기초자료"]);
function linkCol(sh) {
const id = sh.id || sh.name;
if (LINK_COL[id]) return LINK_COL[id];
const t = tagOf(sh);
return t ? colName(colNum(t) + 1) : null;
}
const headRow = (sh) => {
let h = 0;
sh.rows.slice(0, 6).forEach((r, i) => r._k === "head" && (h = i + 1));
return h || 3;
};
const setF = (row, col, v) => ((row._f = row._f || {}), (row._f[col] = v));
const setLink = (row, col, target, text) => {
row[col] = text;
row._links = Object.assign(row._links || {}, { [col]: target });
};
/**
* 바로가기 수식 — =HYPERLINK("#"&시트!이름표2&"!열"&ROW(시트!열행),"글")
* 이름표 칸 수식이 시트 이름을 돌려줘 시트 이름을 바꿔도 따라감(원본 꼴)
*/
function jump(target, col, row, text) {
const n = q(target.name);
return `=HYPERLINK("#"&${n}!${tagOf(target)}2&"!${col}"&ROW(${n}!${col}${row}),"${text}")`;
}
/**
* 바로가기 글 — 코드 · 호표 비고 모드(SH_TYP1=1 · SH_TYP3=1)면 종류 이름 + 목록 차례 5자리(「자재 14」 · 대흥 실측)
* 종류 이름 = Start8x.Ini COD_FMT_1~6(노무·자재·경비·대가·중기·산근)
*/
function codeLabel(by, c) {
const ini = (by._dbf && by._dbf.start_ini) || {};
const hop = String(ini.SH_TYP1 ?? "0") !== "0" && String(ini.SH_TYP3 ?? "2") === "1";
const at = "LMSBXD".indexOf(c[0]);
if (!hop || at < 0) return `${c} →`;
const list = by[LIST_OF[c[0]]];
const n = list
? list.rows.filter((r) => r._k === "item").findIndex((r) => r._code === c) + 1
: 0;
return n > 0 ? `${String(ini[`COD_FMT_${at + 1}`] || "")}${String(n).padStart(5)} →` : `${c} →`;
}
function putJump(row, at, target, col, r, text) {
if (!at || !target || !tagOf(target)) return;
setLink(row, at, `${q(target.name)}!${col}${r}`, text);
setF(row, at, jump(target, col, r, text));
}
// 시트 안 코드 → 행 번호(1부터) — kind 가 같은 행만
function rowsBy(sh, kind) {
const m = {};
if (!sh) return m;
sh.rows.forEach((r, i) => {
if (r._code && (!kind || r._k === kind) && !(r._code in m)) m[r._code] = i + 1;
});
return m;
}
// 머리 행의 비목 열 — 이름 → 열(합계·노무·재료·경비) · BMKNM 차례로 쓴 머리 글에서 찾음
function bimokCols(ini, head, cols) {
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
const out = {};
for (const k of "TLMS") {
const lbl = C.bimokLabel(ini, k);
const c = cols.find((x) => head[x] === lbl);
if (c) out[k] = c;
}
return out;
}
// 코드 → COSTN 단위(M%·L%·…) · 규격
function costnMap(by, key, field) {
if (!by[key]) {
by[key] = {};
for (const r of (by._dbf && by._dbf.costn_rows) || [])
by[key][String(r.CODE || "").trim()] = String(r[field] || "").trim();
}
return by[key];
}
const unitOf = (by) => costnMap(by, "_unit", "UNIT");
const specOf = (by) => costnMap(by, "_spec", "SIZE");
// 코드 머리 → 목록 시트 id
const LIST_OF = {
M: "재료비목록표",
L: "노무비목록표",
S: "경비목록표",
B: "일위대가목록표",
D: "단가산출근거목록표",
X: "중기목록표",
W: "일식견적목록표",
};
const DETAIL_OF = {
일위대가목록표: ["일위대가표", "parent"],
중기목록표: ["중기사용료", "parent"],
단가산출근거목록표: ["단가산출근거", "hopyo"],
};
const COLL_OF = {
재료비수량금액집계표: "재료비목록표",
노무비수량금액집계표: "노무비목록표",
경비수량금액집계표: "경비목록표",
중기시간금액집계표: "중기목록표",
일위대가수량금액집계표: "일위대가목록표",
단가산출근거수량금액집계표: "단가산출근거목록표",
일식견적수량금액집계표: "일식견적목록표",
};
/**
* 목록 층 — 재료비목록표 단가 = 자재단가대비표 적용 단가 · 대가·중기·산근 목록 = 호표 합계 줄 ·
* 수량금액집계표 단가 = 목록 단가 · 비목 집계(중기·대가·산근) = ROUND(수량 × 목록 비목 단가)
*/
function linkLists(by, ini) {
const pc = by.자재단가대비표;
const mat = by.재료비목록표;
if (pc && mat) {
const at = rowsBy(pc, "item");
const lc = linkCol(mat);
mat.rows.forEach((r) => {
const pr = r._k === "item" && at[r._code];
if (!pr) return;
setF(r, "E", `=${q(pc.name)}!O${pr}`);
putJump(r, lc, pc, "A", pr, codeLabel(by, r._code));
});
}
for (const [lid, [did, kind]] of Object.entries(DETAIL_OF)) {
const list = by[lid];
const det = by[did];
if (!list || !det) continue;
const tot = rowsBy(det, "total");
const top = rowsBy(det, kind);
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
const dcols =
did === "단가산출근거"
? bimokCols(ini, det.rows[2] || {}, ["C", "D", "E", "F"])
: bimokCols(ini, det.rows[2] || {}, ["E", "G", "I", "K"]);
const lc = "K";
list.rows.forEach((r) => {
if (r._k !== "item" || !tot[r._code]) return;
for (const k of "TLMS") {
if (!lcols[k] || !dcols[k]) continue;
const dc = did === "단가산출근거" ? dcols[k] : colName(colNum(dcols[k]) + 1);
setF(r, lcols[k], `=${q(det.name)}!${dc}${tot[r._code]}`);
}
const hr = kind === "parent" ? top[r._code] - 1 : top[r._code];
if (hr > 0) putJump(r, lc, det, "A", hr, codeLabel(by, r._code));
});
}
for (const [cid, lid] of Object.entries(COLL_OF)) {
const sh = by[cid];
const list = by[lid];
if (!sh || !list) continue;
const at = rowsBy(list, "item");
const lc = linkCol(sh);
const head = sh.rows.slice(0, 4).find((r) => r._k === "head") || {};
const ccols = bimokCols(ini, head, ["F", "G", "H", "I"]);
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
const single = !ccols.L; // 자원 집계 — 단가 한 칸
sh.rows.forEach((r) => {
const lr = r._k === "item" && at[r._code];
if (!lr) return;
if (single) setF(r, "F", `=${q(list.name)}!E${lr}`);
else {
setF(r, ccols.T, `=${ccols.M}{r}+${ccols.L}{r}+${ccols.S}{r}`);
for (const k of "LMS")
setF(r, ccols[k], `=ROUND(D{r}*${q(list.name)}!${lcols[k]}${lr},0)`);
}
putJump(r, lc, list, "A", lr, codeLabel(by, r._code));
});
}
}
/**
* 구성 호표(일위대가표 · 중기사용료) — 줄 단가 = 목록 단가(노임 시간 환산은 환율및기초자료) ·
* 줄 금액 = IF(수량=0,0,ROUNDDOWN(단가×수량,1)) · % 단위 = ÷100 · 합계 = 같은 열쇠 줄 SUMIF 원 미만 버림
* 총액우선(일위대가 · CHBNM=0) = 끝수 배분 도움 열 S~Y(소수 셋째 끝수 큰 비목부터 · 같으면 OPTNM 차례)
* 숨은 열쇠 — O = 코드_1(줄) · T7_1(합계) · P = T7_1(줄이 속한 합계)
*/
function linkComposition(by, sh, ini) {
if (!sh) return;
const PAIR = { T: ["E", "F"], L: ["G", "H"], M: ["I", "J"], S: ["K", "L"] };
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
const order = C.bimokOrder(ini);
["E", "G", "I", "K"].forEach((u, i) => (PAIR[order[i]] = [u, colName(colNum(u) + 1)]));
const tf = (sh.id || sh.name) === "일위대가표" && String(ini.CHBNM ?? "1") === "0";
const opt = String(ini.OPTNM || "SML")
.toUpperCase()
.split("")
.filter((k) => "MLS".includes(k));
const nonT = order.filter((k) => k !== "T"); // 도움 열 S·T·U 차례 = 단가 열 차례
const help = {};
nonT.forEach((k, i) => (help[k] = { frac: "STU"[i], adj: "WXY"[i] }));
const lab = by.환율및기초자료 ? rowsBy(by.환율및기초자료) : {};
const rowsOf = {};
const sum3 = (side) => `=${PAIR.M[side]}{r}+${PAIR.L[side]}{r}+${PAIR.S[side]}{r}`;
const helpers = (row, base, d, step) => {
for (const k of nonT)
setF(
row,
help[k].frac,
`=ROUND(ROUNDDOWN(${base(PAIR[k][0])},${d + 2})-ROUNDDOWN(${base(PAIR[k][0])},${d}),${d + 2})`,
);
const parts = ["M", "L", "S"].map((k) => `ROUNDDOWN(${base(PAIR[k][0])},${d})`).join("+");
setF(
row,
"V",
`=ROUNDDOWN(ROUND(ROUNDDOWN(${base(PAIR.T[0])},${d})-(${parts}),${d + 2}),${d})`,
);
for (const k of nonT) {
const me = help[k].frac;
const ahead = opt
.slice(0, opt.indexOf(k))
.map((j) => `+IF(${help[j].frac}{r}=${me}{r},1,0)`)
.join("");
setF(
row,
help[k].adj,
`=IF(V{r}<0,-1,1)*IF(COUNTIF(R{r}:U{r},IF(V{r}<0,"<",">")&${me}{r})${ahead}<ROUND(ABS(V{r})/${step},0),${step},0)`,
);
}
};
const isJ = (sh.id || sh.name) === "중기사용료";
const v25 = Number(ini.PRG_V) >= 25;
// 중기 % 코드 줄 중 규격 「주연료의 %」(잡품·잡유) = 앞 연료 줄 금액을 밑수 수식으로(대흥 「=J9」 실측) ·
// 그 밖 % 코드(재료비의 % 등)는 산식 없음(안내 시트 알림)
const fuelPct = (c) => /주연료/.test(specOf(by)[c] || "");
const amounts = (r, pct) => {
setF(r, PAIR.T[0], sum3(0));
setF(r, PAIR.T[1], sum3(1));
for (const k of nonT) {
const [u, a] = PAIR[k];
const core = `ROUNDDOWN(${u}{r}*C{r}${pct ? "/100" : ""},1)`;
setF(r, a, `=IF(C{r}=0,0,${core}${tf ? `+${help[k].adj}{r}` : ""})`);
}
if (tf) helpers(r, (u) => `${u}{r}*C{r}`, 1, 0.1);
};
// 비율 줄 밑수 — 표시 줄(Q=1_01)의 밑수 비목 금액 합 · 25 판부터 칸 더하기(울진 실측)
const prBase = (r, rn, a, marks, inK, outK) => {
const inA = PAIR[inK === "T" ? "M" : inK][1];
const out = PAIR[outK === "T" ? "M" : outK][0];
if (!marks.length) return;
if (v25) setF(r, out, "=" + marks.map((m) => `${inA}${m}`).join("+"));
else setF(r, out, `=SUMIF(Q${a}:Q${rn - 1},"1_01",${inA}${a}:${inA}${rn - 1})`);
};
let first = 0;
let marks = [];
const lc = linkCol(sh);
sh.rows.forEach((r, i) => {
const rn = i + 1;
if (r._k === "parent") {
setF(r, "O", r._code);
first = rn + 1;
marks = [];
return;
}
if (r._k === "text" && first) {
// 글 줄 — 합계 밖 열쇠
setF(r, "O", "_1");
setF(r, "P", "_1");
return;
}
if (r._k === "line" && r._code === "PR" && r._pr) {
setF(r, "O", "PRA01_1");
setF(r, "P", "T7_1");
amounts(r, true);
prBase(r, rn, first, marks, r._pr.in, r._pr.out);
if (r._mark) {
setF(r, "Q", "1_01");
marks.push(rn);
}
return;
}
if (r._k === "line" && r._code && r._code !== "PR") {
const c = r._code;
const pct = r.D === "%";
if (r._mark) {
setF(r, "Q", "1_01");
marks.push(rn);
}
setF(r, "O", `${c}_1`);
if (pct && isJ && fuelPct(c)) {
// 표준 구성 % 줄 — 밑수 = 앞 줄 중 그 비목 금액이 있는 칸 더하기(대흥 「=J9」 실측)
const k = (unitOf(by)[c] || "M%")[0];
const kk = k === "T" ? "M" : k;
const mk = [];
for (let j = first; j < rn; j++) {
const x = sh.rows[j - 1];
if (x._k === "line" && Number(x[PAIR[kk][1]]) !== 0) mk.push(`${PAIR[kk][1]}${j}`);
}
if (mk.length) setF(r, PAIR[kk][0], "=" + mk.join("+"));
}
setF(r, "P", "T7_1");
amounts(r, pct);
const labor = c[0] === "L" && lab[c] && isJ;
const list = labor ? by.환율및기초자료 : by[LIST_OF[c[0]]];
if (!list) return;
const lr = labor
? lab[c]
: (rowsOf[list.name] = rowsOf[list.name] || rowsBy(list, "item"))[c];
if (!lr) return;
if (!pct) {
if (labor) setF(r, PAIR.L[0], `=${q(list.name)}!G${lr}`);
else if ("MLS".includes(c[0])) setF(r, PAIR[c[0]][0], `=${q(list.name)}!E${lr}`);
else {
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
for (const k of nonT) setF(r, PAIR[k][0], `=${q(list.name)}!${lcols[k]}${lr}`);
}
}
putJump(r, lc, list, "A", lr, codeLabel(by, c));
return;
}
if (r._k === "total" && first) {
const a = first;
const b = rn - 1;
const toS = /경비/.test(String(r.A || "")); // 결과적용 비목 경비(SBMOK=S) — 세 비목 합을 경비 칸에
setF(r, "O", "T7_1");
const sumif = (col) => `SUMIF(P${a}:P${b},O{r},${col}${a}:${col}${b})`;
const fin = (k) => `ROUNDDOWN(${sumif(PAIR[k][1])}${tf ? `+${help[k].adj}{r}` : ""},0)`;
setF(r, PAIR.T[1], sum3(1));
for (const k of nonT) {
const [u, am] = PAIR[k];
if (tf) setF(r, u, `=${sumif(am)}`);
if (!toS) setF(r, am, `=${fin(k)}`);
}
if (toS) setF(r, PAIR.S[1], "=" + ["M", "L", "S"].map(fin).join("+"));
if (tf) {
setF(r, PAIR.T[0], sum3(0));
helpers(r, (u) => `${u}{r}`, 0, 1);
}
first = 0;
}
});
}
/**
* 설계내역서 — 묶음 금액 = SUMIF(아래 행 Q 열쇠 = 이 묶음 P 열쇠) · 품목 단가 = 목록 단가 · 금액 = ROUNDDOWN(단가×수량,0)
* 열쇠 — 묶음 P = E단_1(제외 구역 C단_1 · 윗 묶음에 안 더함) · 품목 P = 코드_1(비율 줄 PR_1) · Q = 윗 묶음 열쇠 ·
* 빈 줄 P = CMT_1 · 비율 줄 밑수 표시 R = 1_2_01 · 끝 줄 = 맨 위 열쇠(E10_1) 합
*/
function linkDesign(by, ini) {
const sh = by.설계내역서;
if (!sh) return;
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
const PAIR = {};
const order = C.bimokOrder(ini);
["F", "H", "J", "L"].forEach((u, i) => (PAIR[order[i]] = [u, colName(colNum(u) + 1)]));
const nonT = order.filter((k) => k !== "T");
const sum3 = (side) => `=${PAIR.M[side]}{r}+${PAIR.L[side]}{r}+${PAIR.S[side]}{r}`;
const lc = linkCol(sh);
const rowsOf = {};
let first = 0;
let grpStart = 0;
let last = 0;
sh.rows.forEach((r, i) => {
const rn = i + 1;
if (/^T\d$/.test(r._k || "")) {
if (!first) first = rn;
grpStart = rn + 1;
last = rn;
setF(r, "P", `${r._ex ? "C" : "E"}${r._lv}_1`);
if (!r._ex && r._pk) setF(r, "Q", r._pk);
const end = r._end > rn ? r._end : rn;
for (const k of order) {
const am = PAIR[k][1];
if (end > rn) setF(r, am, `=SUMIF(Q${rn + 1}:Q${end},P{r},${am}${rn + 1}:${am}${end})`);
}
return;
}
if (r._k === "blank") {
setF(r, "P", "CMT_1");
if (r._pk) setF(r, "Q", r._pk);
last = rn;
return;
}
if (r._k !== "item") return;
last = rn;
const c = r._code;
setF(r, "P", c === "PR" ? "PR_1" : `${c}_1`);
setF(r, "Q", r._pk);
if (r._mark) setF(r, "R", "1_2_01");
const pct = r.E === "%";
if (pct) {
setF(r, PAIR.T[0], sum3(0));
setF(r, PAIR.T[1], sum3(1));
for (const k of nonT) setF(r, PAIR[k][1], `=ROUNDDOWN(${PAIR[k][0]}{r}*D{r}/100,0)`);
}
if (c === "PR" && r._pr) {
const inA = PAIR[r._pr.in === "T" ? "M" : r._pr.in][1];
const out = PAIR[r._pr.out === "T" ? "M" : r._pr.out][0];
if (grpStart < rn)
setF(
r,
out,
`=SUMIF(R${grpStart}:R${rn - 1},"1_2_01",${inA}${grpStart}:${inA}${rn - 1})`,
);
return;
}
const list = by[LIST_OF[c[0]]];
if (!list) return;
const lr = (rowsOf[list.name] = rowsOf[list.name] || rowsBy(list, "item"))[c];
if (!lr) return;
if (!pct) {
if ("MLS".includes(c[0])) setF(r, PAIR[c[0]][0], `=${q(list.name)}!E${lr}`);
else {
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
for (const k of nonT)
if (lcols[k]) setF(r, PAIR[k][0], `=${q(list.name)}!${lcols[k]}${lr}`);
}
}
putJump(r, lc, list, "A", lr, codeLabel(by, c));
});
const fin = sh.rows.findIndex((r) => r._k === "final");
if (fin >= 0 && first)
for (const k of order) {
const am = PAIR[k][1];
setF(sh.rows[fin], am, `=SUMIF(Q${first}:Q${last},"E10_1",${am}${first}:${am}${last})`);
}
}
/**
* 총괄내역서 공종 줄 · 순공사비 = 설계내역서 묶음 줄(끝 줄) 금액 참조 + 「내역 →」 바로가기
*/
function linkSummary(by, ini) {
const sm = by.총괄내역서;
const ds = by.설계내역서;
if (!sm || !ds) return;
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
const order = C.bimokOrder(ini);
const dcol = {};
["G", "I", "K", "M"].forEach((c, i) => (dcol[order[i]] = c));
const scol = {};
["D", "E", "F", "G"].forEach((c, i) => (scol[order[i]] = c));
// 설계 묶음 줄 — 같은 단·공종 번호·명칭이 차례로 나오는 행을 맞춤
const groups = ds.rows.map((r, i) => [r, i + 1]).filter(([r]) => /^T\d$/.test(r._k || ""));
let gi = 0;
const fin = ds.rows.findIndex((r) => r._k === "final") + 1;
const lc = linkCol(sm);
sm.rows.forEach((r) => {
let target = 0;
if (/^T\d$/.test(r._k || "")) {
while (gi < groups.length) {
const [g, rn] = groups[gi++];
if (g._k === r._k && g.A === r.A && g.B === r.B) {
target = rn;
break;
}
}
} else if (r._k === "sub" && /가\./.test(String(r.A || "")) && fin > 0) target = fin;
if (!target) return;
for (const k of order) setF(r, scol[k], `=${q(ds.name)}!${dcol[k]}${target}`);
putJump(r, lc, ds, "A", target, "내역 →");
});
}
/**
* 시트마다 — 이름표 칸 수식(파일 안 시트 이름) · 목차 바로가기(머리 끝 행 · 바로가기 열)
*/
function linkFrame(sheets) {
for (const sh of sheets) {
if (NO_LINK.has(sh.name) && sh.name !== "환율및기초자료") continue;
const tag = tagOf(sh);
if (tag) setF(sh.rows[1], tag, FILE_NAME);
if (NO_LINK.has(sh.name)) continue;
const lc = linkCol(sh);
if (!lc) continue;
const row = sh.rows[headRow(sh) - 1];
setLink(row, lc, `${q(TOC)}!B2`, "목차 →");
setF(row, lc, `=HYPERLINK("#${q(TOC)}!B2","목차 →")`);
}
}
// 묶음 전체 잇기 — 시트 id 로 찾음(이름은 SET_REP 제목에 따라 바뀜)
function linkBook(sheets, dbf) {
const ini = (dbf && dbf.start_ini) || {};
const by = { _dbf: dbf };
for (const s of sheets) by[s.id || s.name] = s;
linkFrame(sheets);
linkLists(by, ini);
linkComposition(by, by.일위대가표, ini);
linkComposition(by, by.중기사용료, ini);
linkDesign(by, ini);
linkSummary(by, ini);
return sheets;
}
return { linkBook, specOf, jump, tagOf, linkCol, headRow, q, colName, colNum, TOC, GUIDE, INIT };
})();
if (typeof window !== "undefined") window.STMATE_REPORT_LINK = StmateReportLink;
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportLink;