// ==UserScript== // @name B1 Label Printing // @namespace http://tampermonkey.net/ // @homepage https://github.com/martynas2200/b1-labels // @version 2.1.4 // @description Standard label printing interface // @author Martynas Miliauskas // @match https://www.b1.lt/* // @match https://site.pro/My-Accounting/* // @match https://site.pro/lt/My-Accounting/* // @icon https://b1.lt/favicon.ico // @connect b1.lt // @connect raw.githubusercontent.com // @downloadURL https://raw.githubusercontent.com/martynas2200/b1-labels/main/dist/labels.user.js // @updateURL https://raw.githubusercontent.com/martynas2200/b1-labels/main/dist/labels.user.js // @grant GM.setValue // @grant GM.getValue // @grant unsafeWindow // @grant GM_xmlhttpRequest // @license GNU GPLv3 // ==/UserScript== (function () { 'use strict'; const MINIMAL_TRANSLATIONS = { en: { barcode: 'Barcode', foundXItems: '{1} items found', multipleItemsFound: 'Multiple items found', itemCreated: 'Item created', itemNotFound: 'Item not found!', itemUpdated: 'Item updated', notAllItemsActive: 'Not all items are active; Do you want to proceed?', oddNumberOfItems: 'Odd number of items; Do you want to proceed?', success: 'Success', error: 'Error', failed: 'Failed', loading: 'Loading...', nlabelsToBePrinted: 'labels to be printed', noData: 'No data to print!', invalidId: 'Invalid ID', longTimeAgo: 'a long time ago', twoMonthsAgo: 'two months ago', oneMonthAgo: 'a month ago', threeWeeksAgo: 'three weeks ago', twoWeeksAgo: 'two weeks ago', daysAgo: '{1} days ago', yesterday: 'yesterday', hoursAgo: '{1} hours ago', minutesAgo: '{1} minutes ago', }, lt: { barcode: 'Barkodas', foundXItems: 'Rasta {1} prekių', multipleItemsFound: 'Rasta daugiau negu viena prekė!', itemCreated: 'Prekė sukurta', itemNotFound: 'Prekė nerasta!', itemUpdated: 'Prekė atnaujinta', notAllItemsActive: 'Ne visos prekės aktyvios, ar norite tęsti?', oddNumberOfItems: 'Nelyginis prekių skaičius, ar norite tęsti?', success: 'Sėkmingai atlikta', error: 'Įvyko klaida', failed: 'Nepavyko', loading: 'Kraunama...', nlabelsToBePrinted: 'etiketės spausdinimui', noData: 'Nepakanka duomenų spausdinimui!', invalidId: 'Neteisingas ID', longTimeAgo: 'labai seniai', twoMonthsAgo: 'prieš du mėnesius', oneMonthAgo: 'prieš mėnesį', threeWeeksAgo: 'prieš tris savaites', twoWeeksAgo: 'prieš dvi savaites', daysAgo: 'prieš {1} dienas', yesterday: 'vakar', hoursAgo: 'prieš {1} valandas', minutesAgo: 'prieš {1} minutes', }, }; const currentLanguage = navigator.language.split('-')[0] === 'lt' ? 'lt' : 'en'; const i18n = (key, values = []) => { let translation = MINIMAL_TRANSLATIONS[currentLanguage]?.[key] ?? MINIMAL_TRANSLATIONS.en[key] ?? key; values.forEach((value, index) => { translation = translation.replace(`{${index + 1}}`, value); }); return translation; }; var printStyles = ".is_a_bug{display:none}@media print{body{margin:0;padding:0;max-width:58.3mm;display:inline-flex;flex-wrap:wrap}}.label{all:revert;position:relative;background:#fff;color:#000;height:31.75mm;width:57.15mm;border:.5px solid #ffdfd4;margin:0px;box-sizing:border-box;overflow:hidden}.item{height:19mm;overflow:hidden;padding:4px;font-family:Arial,sans-serif;font-size:initial;line-height:initial}.barcode{white-space:nowrap;position:absolute;bottom:0;z-index:3}.barcode div{font-size:10px;font-family:monospace;margin-left:6px;line-height:1em}.barcode p{margin:0}.dm{width:6mm;height:6mm;transform:rotate(270deg);padding:3px;fill:#000}.subtext{position:absolute;right:2px;bottom:3px;font-family:serif;font-size:18px;font-weight:500;z-index:10;line-height:1em}.price{position:absolute;bottom:21px;font-size:50px;right:0;overflow:hidden;object-position:center;margin-right:3px;line-height:1em;font-family:\"Book Antiqua\",serif;padding:0px 10px}.price-per-unit{color:#777}.barcodeOnly .item{font-size:.9em;margin-right:15px}.barcodeOnly .barcode{left:50%;transform:translateX(-50%) scale(1.4);width:max-content}.barcodeOnly .barcode>div{margin-left:0;font-size:8px;writing-mode:vertical-lr;position:absolute;right:-1.2em;bottom:1.2em}.barcodeOnly .price,.barcodeOnly .price-per-unit,.barcodeOnly .subtext{display:none}.label.fridge::before{content:\"\";display:block;position:absolute;border-top:.5px dashed #ff9b79;width:100%;bottom:7mm}.label.fridge .item{font-size:15px;height:12mm}.label.fridge .barcode p,.label.fridge .price-per-unit{display:none}.label.fridge .subtext{font-family:\"Book Antiqua\",serif;font-size:1em;bottom:11mm;left:0;right:unset;background:#888;color:#fff;padding:0px 3px;margin-left:6px;border-radius:3px;padding-top:2px}.label.fridge .price,.label.fridge .barcode{bottom:8mm;line-height:.9em}.label.half{width:28.575mm;display:block;border-right:.5px dashed #ff9b79}.label.half+.label.half:nth-of-type(even){border-right-style:solid;border-left-style:none}.label.half .item{font-size:.75em;font-size:12px;text-align:center}.label.half .subtext{padding:0px 3px;border-radius:3px;font-size:.95em}.label.half .price{right:0;left:0;margin:0px 3px;padding:0px;text-align:center;font-size:38px}.label.half .barcode.dm{transform:none;bottom:1}.label.half .barcode p{display:none}.label.weight{display:grid;grid-template-columns:min-content 1fr;align-items:center;justify-content:space-between;padding:5px;box-sizing:border-box;writing-mode:vertical-rl;grid-column-gap:2px;grid-row-gap:0px;font-family:\"Arial\"}.label.weight .barcode{grid-column:1;position:static;width:8mm;height:8mm;transform:unset}.label.weight .item{grid-column:span 2;text-align:left;font-size:11pt;font-size:10pt;max-width:60pt;padding:0px;height:unset}.label.weight .price{all:revert;grid-row:2;grid-column:span 2;text-align:center;justify-self:center;align-self:end;font-size:16pt;font-weight:bold;line-height:1em;font-family:\"Book Antiqua\",serif;margin-right:4px}.label.weight .price::after{content:\" €\"}.label.weight .kg-price{grid-column:2;justify-self:center;align-self:end}.label.weight .kg-text{grid-column:2}.label.weight .kg-text,.label.weight .weight-text{justify-self:center;align-self:baseline;color:#999;font-size:.8em;line-height:.6}.label.weight .weight-text{grid-column:1}.label.weight .weight{grid-column:1;justify-self:center;align-self:end}.label.weight .expiry{grid-column:2;text-align:center;justify-self:center;align-self:center;line-height:.9;font-size:.8em;color:#000}.label.weight .expiry::before{content:\"Geriausia iki \";color:#444;display:inline;font-family:Arial}.label.weight.half{display:grid;writing-mode:unset}.label.weight.half .price{font-size:18px}.label.weight.half *:not(.price){font-size:9pt}.label.weight.half .item{max-width:unset;max-height:11mm}.label.weight.half .dm{grid-row:3/span 3}.label.weight.half .weight{grid-column:2}.label.weight.half .expiry::before{content:\" \";display:inline-block;position:static;background-repeat:no-repeat;background-image:url(\"data:image/svg+xml,%3Csvg fill='%23000000' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='15px' height='15px' viewBox='0 0 612 612' xml:space='preserve'%3E%3Cg%3E%3Cg%3E%3Cpath d='M612,463.781c0-70.342-49.018-129.199-114.75-144.379c-10.763-2.482-21.951-3.84-33.469-3.84 c-3.218,0-6.397,0.139-9.562,0.34c-71.829,4.58-129.725,60.291-137.69,131.145c-0.617,5.494-0.966,11.073-0.966,16.734 c0,10.662,1.152,21.052,3.289,31.078C333.139,561.792,392.584,612,463.781,612C545.641,612,612,545.641,612,463.781z M463.781,561.797c-54.133,0-98.016-43.883-98.016-98.016s43.883-98.016,98.016-98.016s98.016,43.883,98.016,98.016 S517.914,561.797,463.781,561.797z'/%3E%3Cpolygon points='482.906,396.844 449.438,396.844 449.438,449.438 396.844,449.438 396.844,482.906 482.906,482.906 482.906,449.438 482.906,449.438 '/%3E%3Cpath d='M109.969,0c-9.228,0-16.734,7.507-16.734,16.734v38.25v40.641c0,9.228,7.506,16.734,16.734,16.734h14.344 c9.228,0,16.734-7.507,16.734-16.734V54.984v-38.25C141.047,7.507,133.541,0,124.312,0H109.969z'/%3E%3Cpath d='M372.938,0c-9.228,0-16.734,7.507-16.734,16.734v38.25v40.641c0,9.228,7.507,16.734,16.734,16.734h14.344 c9.228,0,16.734-7.507,16.734-16.734V54.984v-38.25C404.016,7.507,396.509,0,387.281,0H372.938z'/%3E%3Cpath d='M38.25,494.859h236.672c-2.333-11.6-3.572-23.586-3.572-35.859c0-4.021,0.177-7.999,0.435-11.953H71.719 c-15.845,0-28.688-12.843-28.688-28.688v-229.5h411.188v88.707c3.165-0.163,6.354-0.253,9.562-0.253 c11.437,0,22.61,1.109,33.469,3.141V93.234c0-21.124-17.126-38.25-38.25-38.25h-31.078v40.641c0,22.41-18.23,40.641-40.641,40.641 h-14.344c-22.41,0-40.641-18.231-40.641-40.641V54.984H164.953v40.641c0,22.41-18.231,40.641-40.641,40.641h-14.344 c-22.41,0-40.641-18.231-40.641-40.641V54.984H38.25C17.126,54.984,0,72.111,0,93.234v363.375 C0,477.733,17.126,494.859,38.25,494.859z'/%3E%3Ccircle cx='134.774' cy='260.578' r='37.954'/%3E%3Ccircle cx='248.625' cy='260.578' r='37.954'/%3E%3Ccircle cx='362.477' cy='260.578' r='37.954'/%3E%3Ccircle cx='248.625' cy='375.328' r='37.953'/%3E%3Ccircle cx='134.774' cy='375.328' r='37.953'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\");height:15px;width:15px}.label.weight.half .expiry{display:flex;align-items:center}.label.weight.half .manufacturer,.label.weight.half .weight-text,.label.weight.half .kg-text,.label.weight.half .package{display:none}.label.weight .manufacturer,.label.weight .description{font-size:.7em;grid-column:span 2}.label.weight .package{font-size:.7em;grid-column:span 2}.label.weight .package{grid-row:3;grid-column:span 2;text-align:center;justify-self:center;align-self:center;margin-right:-5px;margin-left:5px;color:#444;font-size:10px;line-height:.8em}del{color:gray;position:relative;text-decoration:none;position:absolute;bottom:0;right:5px;font-size:1.4em;line-height:1em}del:after{content:\"\";display:block;position:absolute;width:110%;height:2px;border-radius:1px;background:darkred;top:9px;left:-5%;transform:skewY(-14deg)}"; const getDataMatrixMat = (text, rect = false) => { var enc = [], cw = 0, ce = 0; function push(val) { cw = 40 * cw + val; if (ce++ == 2) { enc.push(++cw >> 8); enc.push(cw & 255); ce = cw = 0; } } var cost = [ function (c) { return ((c - 48) & 255) < 10 ? 6 : c < 128 ? 12 : 24; }, function (c) { return ((c - 48) & 255) < 10 || ((c - 65) & 255) < 26 || c == 32 ? 8 : c < 128 ? 16 : 16 + cost[1](c & 127); }, function (c) { return ((c - 48) & 255) < 10 || ((c - 97) & 255) < 26 || c == 32 ? 8 : c < 128 ? 16 : 16 + cost[2](c & 127); }, function (c) { return ((c - 48) & 255) < 10 || ((c - 65) & 255) < 26 || c == 32 || c == 13 || c == 62 || c == 42 ? 8 : 1e9; }, function (c) { return c >= 32 && c < 95 ? 9 : 1e9; }, function (c) { return 12; } ]; var latch = [0, 24, 24, 24, 21, 25]; var count = [0, 12, 12, 12, 12, 25]; var c, i, p, cm = 0, nm = 0; var bytes = []; bytes[text.length] = count.slice(); for (p = text.length; p-- > 0;) { for (c = 1e9, i = 0; i < count.length; i++) { count[i] += cost[i](text.charCodeAt(p)); c = Math.min(c, Math.ceil(count[i] / 12) * 12); } if (cost[0](text.charCodeAt(p)) > 6) count[0] = Math.ceil(count[0] / 12) * 12; for (i = 0; i < count.length; i++) if (c + latch[i] < count[i]) count[i] = c + latch[i]; bytes[p] = count.slice(); } for (p = 0;; cm = nm) { c = bytes[p][cm] - latch[cm]; if (p + [0, 2, 2, 2, 3, 0][cm] >= text.length) nm = 0; else for (i = cost.length; i-- > 0;) if (Math.ceil((bytes[p + 1][i] + cost[i](text.charCodeAt(p))) / 12) * 12 == c) nm = i; if (cm != nm && cm > 0) if (cm < 4) enc.push(254); else if (cm == 4) enc.push(31 | cw & 255); else { if (ce > 249) enc.push((ce / 250 + 250 + (149 * (enc.length + 1)) % 255) & 255); enc.push((ce % 250 + (149 * (enc.length + 1)) % 255 + 1) & 255); for (; ce > 0; ce--) enc.push((text.charCodeAt(p - ce) + (149 * (enc.length + 1)) % 255 + 1) & 255); } if (p >= text.length) break; if (cm != nm) cw = ce = 0; if (cm != nm && nm > 0) enc.push([230, 239, 238, 240, 231][nm - 1]); if (nm == 0) { c = text.charCodeAt(p++); i = (c - 48) & 255; if (i < 10 && p < text.length && ((text.charCodeAt(p) - 48) & 255) < 10) enc.push(i * 10 + text.charCodeAt(p++) - 48 + 130); else { if (c > 127) enc.push(235); enc.push((c & 127) + 1); } if (cm == 4 || ce < 0) ce--; } else if (nm < 4) { var set = [[31, 0, 32, 119, 47, 133, 57, 179, 64, 173, 90, 207, 95, 277, 127, 386, 255, 1], [31, 0, 32, 119, 47, 133, 57, 179, 64, 173, 90, 258, 95, 277, 122, 335, 127, 386, 255, 1], [13, 55, 32, 119, 42, 167, 57, 179, 62, 243, 90, 207, 255, 3]][nm - 1]; do { c = text.charCodeAt(p++); if (c > 127) { push(1); push(30); c &= 127; } for (i = 0; c > set[i]; i += 2) ; if ((set[i + 1] & 3) < 3) push(set[i + 1] & 3); push(c - (set[i + 1] >> 2)); } while (ce > 0); } else if (nm == 4) { if (ce > 0) enc.push(255 & cw + (text.charCodeAt(p++) & 63)); for (cw = ce = 0; ce < 3; ce++) cw = 64 * (cw + (text.charCodeAt(p++) & 63)); enc.push(cw >> 16); enc.push((cw >> 8) & 255); } else { p++; ce++; } } var el = enc.length; var h, w, nc = 1, nr = 1, fw, fh; var j = -1, l, r, s, b = 1, k; if (ce == -1 || (cm && cm < 5)) nm = 1; if (rect && el - nm < 50) { k = [16, 7, 28, 11, 24, 14, 32, 18, 32, 24, 44, 28]; do { w = k[++j]; h = 6 + (j & 12); l = w * h / 8; } while (l - k[++j] < el - nm); if (w > 25) nc = 2; } else { w = h = 6; i = 2; k = [5, 7, 10, 12, 14, 18, 20, 24, 28, 36, 42, 48, 56, 68, 84, 112, 144, 192, 224, 272, 336, 408, 496, 620]; do { if (++j == k.length) return []; if (w > 11 * i) i = 4 + i & 12; w = h += i; l = (w * h) >> 3; } while (l - k[j] < el - nm); if (w > 27) nr = nc = 2 * Math.floor(w / 54) + 2; if (l > 255) b = 2 * (l >> 9) + 2; } s = k[j]; if (l - s + 1 == el && nm > 0) { el--; if (ce == -1) enc[el - 1] ^= 31 ^ (enc[el] - 1) & 63; } fw = w / nc; fh = h / nr; if (el < l - s) enc[el++] = 129; while (el < l - s) enc[el++] = (((149 * el) % 253) + 130) % 254; s /= b; var rs = new Array(70), rc = new Array(70); var lg = new Array(256), ex = new Array(255); for (j = 1, i = 0; i < 255; i++) { ex[i] = j; lg[j] = i; j += j; if (j > 255) j ^= 301; } for (rs[s] = 0, i = 1; i <= s; i++) for (j = s - i, rs[j] = 1; j < s; j++) rs[j] = rs[j + 1] ^ ex[(lg[rs[j]] + i) % 255]; for (c = 0; c < b; c++) { for (i = 0; i <= s; i++) rc[i] = 0; for (i = c; i < el; i += b) for (j = 0, k = rc[0] ^ enc[i]; j < s; j++) rc[j] = rc[j + 1] ^ (k ? ex[(lg[rs[j]] + lg[k]) % 255] : 0); for (i = 0; i < s; i++) enc[el + c + i * b] = rc[i]; } var mat = Array(h + 2 * nr).fill(null).map(function () { return []; }); for (i = 0; i < w + 2 * nc; i += fw + 2) for (j = 0; j < h; j++) { mat[j + (j / fh | 0) * 2 + 1][i] = 1; if ((j & 1) == 1) mat[j + (j / fh | 0) * 2][i + fw + 1] = 1; } for (i = 0; i < h + 2 * nr; i += fh + 2) for (j = 0; j < w + 2 * nc; j++) { mat[i + fh + 1][j] = 1; if ((j & 1) == 0) mat[i][j] = 1; } s = 2; c = 0; r = 4; for (i = 0; i < l; r -= s, c += s) { if (r == h - 3 && c == -1) k = [w, 6 - h, w, 5 - h, w, 4 - h, w, 3 - h, w - 1, 3 - h, 3, 2, 2, 2, 1, 2]; else if (r == h + 1 && c == 1 && (w & 7) == 0 && (h & 7) == 6) k = [w - 2, -h, w - 3, -h, w - 4, -h, w - 2, -1 - h, w - 3, -1 - h, w - 4, -1 - h, w - 2, -2, -1, -2]; else { if (r == 0 && c == w - 2 && (w & 3)) continue; if (r < 0 || c >= w || r >= h || c < 0) { s = -s; r += 2 + s / 2; c += 2 - s / 2; while (r < 0 || c >= w || r >= h || c < 0) { r -= s; c += s; } } if (r == h - 2 && c == 0 && (w & 3)) k = [w - 1, 3 - h, w - 1, 2 - h, w - 2, 2 - h, w - 3, 2 - h, w - 4, 2 - h, 0, 1, 0, 0, 0, -1]; else if (r == h - 2 && c == 0 && (w & 7) == 4) k = [w - 1, 5 - h, w - 1, 4 - h, w - 1, 3 - h, w - 1, 2 - h, w - 2, 2 - h, 0, 1, 0, 0, 0, -1]; else if (r == 1 && c == w - 1 && (w & 7) == 0 && (h & 7) == 6) continue; else k = [0, 0, -1, 0, -2, 0, 0, -1, -1, -1, -2, -1, -1, -2, -2, -2]; } for (el = enc[i++], j = 0; el > 0; j += 2, el >>= 1) { if (el & 1) { var x = c + k[j], y = r + k[j + 1]; if (x < 0) { x += w; y += 4 - ((w + 4) & 7); } if (y < 0) { y += h; x += 4 - ((h + 4) & 7); } mat[y + 2 * (y / fh | 0) + 1][x + 2 * (x / fw | 0) + 1] = 1; } } } for (i = w; i & 3; i--) mat[i][i] = 1; return mat; }; const toPath = (mat) => { var path = "", x, y; mat.forEach(function (y) { y.unshift(0); }); mat.push([]); mat.unshift([]); for (;;) { for (y = 0; y + 2 < mat.length; y++) if ((x = mat[y + 1].indexOf(1) - 1) >= 0 || (x = mat[y + 1].indexOf(5) - 1) >= 0) break; if (y + 2 == mat.length || path.length > 1e7) return path; var c = mat[y + 1][x + 1] >> 2, p = ""; for (var x0 = x, y0 = y, d = 1; p.length < 1e6;) { do x += 2 * d - 1; while ((mat[y][x + d] ^ mat[y + 1][x + d]) & mat[y + d][x + d] & 1); d ^= mat[y + d][x + d] & 1; do mat[d ? ++y : y--][x + 1] ^= 2; while ((mat[y + d][x] ^ mat[y + d][x + 1]) & mat[y + d][x + 1 - d] & 1); if (x == x0 && y == y0) break; d ^= 1 ^ mat[y + d][x + 1 - d] & 1; if (c) p = "V" + y + "H" + x + p; else p += "H" + x + "V" + y; } path += "M" + x + " " + y + p + (c ? "V" + y : "H" + x) + "Z"; for (d = 0, y = 1; y < mat.length - 1; y++) for (x = 1; x < mat[y].length; x++) { d ^= (mat[y][x] >> 1) & 1; mat[y][x] = 5 * d ^ mat[y][x] & 5; } } }; class Code128 { text; constructor(text) { this.text = text; } encode() { var t = 3, enc = [], i, j, c, mat = []; for (i = 0; i < this.text.length; i++) { c = this.text.charCodeAt(i); if (t != 2) { for (j = 0; j + i < this.text.length; j++) if (this.text.charCodeAt(i + j) - 48 >>> 0 > 9) break; if ((j > 1 && i == 0) || (j > 3 && (i + j < this.text.length || (j & 1) == 0))) { enc.push(i == 0 ? 105 : 99); t = 2; } } if (t == 2) if (c - 48 >>> 0 < 10 && i + 1 < this.text.length && this.text.charCodeAt(i + 1) - 48 >>> 0 < 10) enc.push(+this.text.substr(i++, 2)); else t = 3; if (t != 2) { if (t > 2 || ((c & 127) < 32 && t) || ((c & 127) > 95 && !t)) { for (j = t > 2 ? i : i + 1; j < this.text.length; j++) if ((this.text.charCodeAt(j) - 32) & 64) break; j = j == this.text.length || (this.text.charCodeAt(j) & 96) ? 1 : 0; enc.push(i == 0 ? 103 + j : j != t ? 101 - j : 98); t = j; } if (c > 127) enc.push(101 - t); enc.push(((c & 127) + 64) % 96); } } if (i == 0) enc.push(103); j = enc[0]; for (i = 1; i < enc.length; i++) j += i * enc[i]; enc.push(j % 103); enc.push(106); c = [358, 310, 307, 76, 70, 38, 100, 98, 50, 292, 290, 274, 206, 110, 103, 230, 118, 115, 313, 302, 295, 370, 314, 439, 422, 406, 403, 434, 410, 409, 364, 355, 283, 140, 44, 35, 196, 52, 49, 324, 276, 273, 220, 199, 55, 236, 227, 59, 443, 327, 279, 372, 369, 375, 428, 419, 395, 436, 433, 397, 445, 289, 453, 152, 134, 88, 67, 22, 19, 200, 194, 104, 97, 26, 25, 265, 296, 477, 266, 61, 158, 94, 79, 242, 122, 121, 466, 458, 457, 367, 379, 475, 188, 143, 47, 244, 241, 468, 465, 239, 247, 431, 471, 322, 328, 334, 285]; for (t = i = 0; i < enc.length; i++, t++) { mat[t++] = 1; for (j = 256; j > 0; j >>= 1, t++) if (c[enc[i]] & j) mat[t] = 1; } mat[t++] = mat[t] = 1; return [mat]; } toHtml(mat, size = 3, blocks = 5) { if (!Array.isArray(size)) size = [size || 3, size || 3]; let s = "barcode" + size[0] + size[1], b, ss; let html = "
"; for (i = 0; i < mat.length; i++) for (j = 0; j < mat[i].length;) { if (i && !j) html += "
"; for (b = 0; j < mat[i].length; b++, j++) if (!mat[i][j] || b + 1 == blocks) break; for (ss = 0; j < mat[i].length; ss++, j++) if (mat[i][j] || ss + 1 == blocks) break; html += "
"; } return html + "
"; } } class LabelGenerator { items = []; success = false; type; constructor(data = undefined, type = 'normal') { this.type = type; if (data == null) ; else if (data instanceof Promise) { void data.then((data) => { this.items = data; this.print(); }); } else { this.items = data; this.print(); } } print() { this.items = this.items.filter((item) => item.barcode != null); if (!this.isAllItemsActive()) { if (!confirm(i18n('notAllItemsActive'))) { return; } } if (this.type == 'half' && this.items.length % 2 != 0) { if (!confirm(i18n('oddNumberOfItems'))) { return; } } if (this.items.length > 0) { void this.printLabelsUsingBrowser(this.items); } else { alert(i18n('noData')); } } isAllItemsActive() { return this.items.every((item) => item.isActive); } makeUpperCaseBold(text) { const regex = /("[^"]+"|[A-ZŽĄČĘĖĮŠŲŪ]{3,})/g; return text.replace(regex, '$1'); } static getPricePerUnit(item) { const regex = /(?:,?\s*)?(?:(\d+)\s*x\s*)?(\d+(\.\d+)?(?:,\d+)?)[\s]*(k?g|m?l|vnt|pak|rul)\b/i; const match = item.name.match(regex); if (match) { const match2 = item.name.replace(match[0], '').match(regex); if (match2) { return null; } const multiplier = match[1] ? parseInt(match[1]) : 1; const amount = parseFloat(match[2].replace(',', '.')) * multiplier; const unit = match[4].replace('.', '').toLowerCase(); let pricePerUnit; if (unit === 'g' || unit === 'ml') { pricePerUnit = (item.priceWithVat / (amount / 1000)).toFixed(2) + (unit === 'ml' ? ' €/l' : ' €/kg'); } else { pricePerUnit = (item.priceWithVat / amount).toFixed(2) + ' €/' + unit; } if (parseFloat(pricePerUnit) === item.priceWithVat) { return null; } return pricePerUnit; } return null; } generateLabel(data, type = this.type) { const label = document.createElement('div'); label.className = 'label'; if (data.weight != null) { return this.generateWeightLabel(data, type === 'half'); } else if (type !== 'normal') { label.classList.add(type); } label.appendChild(this.createDivWithClass('item', this.makeUpperCaseBold(data.name), true)); if (data.barcode != null && type !== 'half') { label.appendChild(this.createCode123Div(data)); } else if (data.barcode != null) { label.appendChild(this.createDMDiv(data.barcode)); } label.appendChild(this.createDivWithClass('price', this.getItemPrice(data))); if (data.packageCode != null) { const packagePrice = 0.1 * data.packageQuantity; label.appendChild(this.createDivWithClass('subtext', 'Tara +' + packagePrice.toFixed(2))); } else if (data.measurementUnitCanBeWeighed == true) { label.appendChild(this.createDivWithClass('subtext', '/ 1 ' + data.measurementUnitName)); } return label; } getItemPrice(data) { if (data.priceWithVat == null || data.priceWithVat === 0) { return ''; } if (typeof data.priceWithVat === 'number') { return data.priceWithVat.toFixed(2); } else { return parseFloat(data.priceWithVat).toFixed(2).toString(); } } createCode123Div(data) { const barcode = document.createElement('div'); barcode.className = 'barcode'; barcode.appendChild(this.createDivWithClass('barcode-text', data.barcode)); const code128 = new Code128(data.barcode); const p = document.createElement('p'); p.innerHTML = code128.toHtml(code128.encode(), this.type == 'barcodeOnly' ? [1, 50] : [1, 15]); barcode.appendChild(p); return barcode; } generateWeightLabel(data, half = false) { const label = document.createElement('div'); if (data.weight == null || data.totalPrice == null || data.priceWithVat == null || data.barcode == null || data.barcode.length > 13) { return label; } label.className = 'label weight' + (half ? ' half' : ''); const elements = [ { className: 'item', text: data.name }, { className: 'price', text: half && data.addPackageFee ? (data.totalPrice + 0.01).toFixed(2) : data.totalPrice.toFixed(2), }, { className: 'weight', text: (data.measurementUnitCanBeWeighed ? Number(data.weight).toFixed(3) : data.weight.toString()) + (half ? ' ' + data.measurementUnitName : ''), }, { className: 'kg-price', text: data.priceWithVat.toFixed(2) + (half ? ' €/kg' : ''), }, { className: 'weight-text', text: data.measurementUnitName }, { className: 'kg-text', text: '€/' + data.measurementUnitName }, ]; if (data.addPackageFee && !half) { elements.push({ className: 'package', text: '+ 0,01 (fas. maišelis)' }); } elements.forEach(({ className, text }) => { label.appendChild(this.createDivWithClass(className, text)); }); const barcodeString = (data.addPackageFee ? '1102\t1\n' : '') + this.createPackedItemBarcode(data) + '\t1\n\r'; label.appendChild(this.createDMDiv(barcodeString)); if (data.expiryDate != null) { const date = new Date(data.expiryDate).toLocaleDateString('lt-LT', { month: '2-digit', day: '2-digit', }); label.appendChild(this.createDivWithClass('expiry', date)); } if (data.addManufacturer == true && data.manufacturerName != null) { label.appendChild(this.createDivWithClass('manufacturer', data.manufacturerName)); } return label; } createDivWithClass(className, text, raw = false) { const div = document.createElement('div'); div.className = className; if (raw) { div.innerHTML = text; } else { div.textContent = text; } return div; } createPackageBarcode(items) { if (items.length < 1) { throw new Error('No items to create package barcode'); } let barcodeString = ''; items.forEach((item) => { if (item.barcode == null) { throw new Error('Item has no barcode'); } const quantity = item.weight != null ? item.weight : 1; barcodeString += `${item.barcode}\t${quantity}\n`; }); barcodeString += '\r'; return barcodeString; } createPackedItemBarcode(data) { if (data.barcode == null) { throw new Error('Item has no barcode'); } return ('2200' + data.barcode.padStart(13, '0') + data.weight.toFixed(3).replace('.', '').padStart(4, '0')); } createDMDiv(barcodeString, big = false) { if (typeof barcodeString !== 'string') { throw new Error('Barcode string must be a string'); } else if (barcodeString.length < 1) { throw new Error('Barcode string cannot be empty'); } const barcode = document.createElement('div'); barcode.className = 'barcode dm'; const svgNS = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(svgNS, 'svg'); const path = document.createElementNS(svgNS, 'path'); const matrix = getDataMatrixMat(barcodeString); const actualSize = matrix.length; path.setAttribute('transform', 'scale(1)'); path.setAttribute('d', toPath(matrix)); svg.appendChild(path); svg.setAttribute('class', 'datamatrix'); svg.setAttribute('viewBox', `0 0 ${actualSize} ${actualSize}`); if (big === true) { svg.style.width = '100%'; svg.style.height = '100%'; svg.style.maxWidth = '60px'; svg.style.maxHeight = '60px'; } else { svg.style.width = '100%'; svg.style.height = '100%'; svg.style.maxWidth = '30px'; svg.style.maxHeight = '30px'; } svg.style.imageRendering = 'pixelated'; svg.style.shapeRendering = 'crispEdges'; barcode.appendChild(svg); return barcode; } isItInAppMode() { return (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches); } async printLabelsUsingBrowser(data) { const labels = data.map((item) => this.generateLabel(item)); const popup = window.open('', '_blank', this.isItInAppMode() ? 'width=250,height=300' : 'width=800,height=600'); if (popup == null) { alert('Please allow popups for this site'); return; } popup.document.title = `${labels.length} ${i18n('nlabelsToBePrinted')}`; popup.document.head.appendChild(this.createStyleElement()); labels.forEach((label) => { popup.document.body.appendChild(label); }); this.success = true; popup.addEventListener('afterprint', () => { popup.close(); }); popup.print(); } createStyleElement() { const style = document.createElement('style'); style.innerHTML = `${printStyles}`; return style; } } class AngularServiceLocator { static injector = null; static getInjector() { if (this.injector) { return this.injector; } const appElement = document.querySelector('[ng-app]'); if (!appElement) { throw new Error('Angular app not found'); } this.injector = angular.element(appElement).injector(); return this.injector; } static getService(serviceName) { return this.getInjector().get(serviceName); } } class UINotification { notificationService; constructor() { try { this.notificationService = AngularServiceLocator.getService('Notification'); } catch (error) { console.error('Failed to get Notification service', error); this.notificationService = { info: (options) => alert(options), error: (options) => alert(options), success: (options) => alert(options), warning: (options) => alert(options), primary: (options) => alert(options) }; } } info(options) { this.notificationService.info(options); } error(options) { this.notificationService.error(options); } success(options) { this.notificationService.success(options); } warning(options) { this.notificationService.warning(options); } primary(options) { this.notificationService.primary(options); } } function calculateTotalPrice(priceWithVat, quantity) { if (priceWithVat == null || quantity == null) { return 0; } const totalPrice = priceWithVat * quantity; return Math.round((totalPrice + Number.EPSILON) * 100) / 100; } function modalHTML(i18n) { return ` `; } class VirtualKeyboard { options; scope; i18nFunction; constructor(options = {}, i18nFunction) { this.i18nFunction = i18nFunction; this.options = { initialValue: '', placeholder: '', title: 'Enter Value', maxLength: 10, allowDecimal: true, ...options, }; this.scope = { value: this.options.initialValue || '', replaceInitialValue: this.options.initialValue !== undefined && this.options.initialValue !== null, visible: false, title: this.options.title || 'Enter Value', placeholder: this.options.placeholder || '', key: this.handleKey.bind(this), confirm: this.handleConfirm.bind(this), cancel: this.handleCancel.bind(this), hide: this.hide.bind(this), i18n: this.i18nFunction, }; } getScope() { return this.scope; } show() { this.scope.visible = true; this.scope.value = ''; } hide() { this.scope.visible = false; } handleKey(input) { if (input === 'd') { this.scope.value = this.scope.value.slice(0, -1); } else if (input === 'c') { this.scope.value = ''; } else if (input === '.') { if (this.options.allowDecimal && !this.scope.value.includes('.')) { this.scope.value += input; } } else if (/^\d$/.test(input)) { if (this.scope.replaceInitialValue) { this.scope.value = input; this.scope.replaceInitialValue = false; } else if (!this.options.maxLength || this.scope.value.length < this.options.maxLength) { this.scope.value += input; } } if (this.options.onValueChange) { this.options.onValueChange(this.scope.value); } } handleConfirm() { if (this.options.onConfirm) { this.options.onConfirm(this.scope.value); } this.hide(); } handleCancel() { if (this.options.onCancel) { this.options.onCancel(); } this.hide(); } setValue(value) { this.scope.value = value; if (this.options.onValueChange) { this.options.onValueChange(this.scope.value); } } getValue() { return this.scope.value; } updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; this.scope.title = this.options.title || 'Enter Value'; this.scope.placeholder = this.options.placeholder || ''; } } class WeightLabelModal { modalScope; modalService; notifier; controller; virtualKeyboard; constructor(modalService, notifier, controller) { this.modalService = modalService; this.notifier = notifier; this.controller = controller; this.virtualKeyboard = new VirtualKeyboard({ allowDecimal: true, maxLength: 8, onValueChange: (value) => { if (this.modalScope.item) { this.modalScope.item.weight = value; this.handleWeightChange(); } }, }, i18n); this.modalScope = { item: null, weight: '', virtualKeyboardVisible: this.controller !== undefined, addButton: this.controller !== undefined, hideVirtualKeyboard: this.hideVirtualKeyboard.bind(this), handleWeightChange: this.handleWeightChange.bind(this), key: this.key.bind(this), add: this.add.bind(this), print: this.print.bind(this), picker: this.showPicker.bind(this), i18n: i18n, }; } hideVirtualKeyboard() { this.modalScope.virtualKeyboardVisible = false; } key(input) { if (!this.modalScope.item) { return; } if (input === 'd' && this.modalScope.item.weight) { this.modalScope.item.weight = this.modalScope.item.weight .toString() .slice(0, -1); } else if (input === 'c') { this.modalScope.item.weight = ''; } else if (input !== 'd') { this.modalScope.item.weight = (this.modalScope.item.weight || '') + input; } this.modalScope.handleWeightChange(); } show(item) { if (!item || !item.name || !item.priceWithVat) { this.notifier.error(i18n('noData')); return Promise.reject(); } this.setupModalItem(item); return this.modalService.showModal({ template: modalHTML(), scopeProperties: this.modalScope, size: 'md', windowClass: 'weight-label-modal', }); } setupModalItem(item) { this.modalScope.item = { ...item, weight: '', addManufacturer: false, addPackageFee: true, }; } getWeightItem() { const item = { ...this.modalScope.item }; if (!item || !item.weight) { this.notifier.error(i18n('missingWeight')); return null; } item.weight = item.measurementUnitCanBeWeighed ? this.gToKg(parseFloat(item.weight.toString())) : parseFloat(item.weight.toString()); if (item.weight > 9.999) { this.notifier.error(i18n('maxWeight')); return null; } item.addManufacturer = !!item.addManufacturer && !!item.manufacturerName?.length; return item; } add() { if (this.controller !== undefined) { const item = this.getWeightItem(); if (item) { void this.controller?.processItem(item, true); } } } print() { const item = this.getWeightItem(); if (item) { this.notifier.success({ title: i18n('printJobIsSent'), message: `${item.weight}${item.measurementUnitName}`, }); new LabelGenerator([item]); } } handleWeightChange() { if (!this.modalScope.item) { return; } this.modalScope.item.totalPrice = calculateTotalPrice(this.modalScope.item.priceWithVat, this.modalScope.item.measurementUnitCanBeWeighed ? this.gToKg(Number(this.modalScope.item.weight || 0)) : Number(this.modalScope.item.weight || 0)); } gToKg(weight) { return weight / 1000; } showPicker(event) { const target = event.target; if (target?.showPicker) { target.showPicker(); } } } class ModalService { $uibModal; $rootScope; modalInstance = null; constructor() { const injector = angular.element(document.body).injector(); this.$uibModal = injector.get('$uibModal'); this.$rootScope = injector.get('$rootScope'); } async showModal(config) { const modalScope = this.$rootScope.$new(true); if (config.scopeProperties) { Object.defineProperties(modalScope, Object.entries(config.scopeProperties).reduce((acc, [key, value]) => ({ ...acc, [key]: { get: () => config.scopeProperties[key], set: (v) => config.scopeProperties[key] = v, enumerable: true, configurable: true } }), {})); } this.modalInstance = this.$uibModal.open({ animation: true, template: config.template, scope: modalScope, size: config.size || 'lg', backdrop: config.backdrop || 'static', windowClass: config.windowClass || '', }); modalScope.closeModal = () => { this.modalInstance.close(); modalScope.$destroy(); config.onClose?.(); }; return this.modalInstance.result; } } class AdminPrintUserscript { pageReady = false; notification = new UINotification(); modal = new ModalService(); currentUrl; constructor() { this.currentUrl = window.location.pathname; this.init(); void this.handleUrlChange(null, this.currentUrl); console.debug('LabelsUserscript initialized'); } init() { this.overrideHistoryMethods(); this.setupPopStateListener(); } overrideHistoryMethods() { const originalPushState = history.pushState; history.pushState = (state, title, url) => { const previousUrl = this.currentUrl; const result = originalPushState.apply(history, [state, title, url]); this.currentUrl = window.location.pathname; void this.handleUrlChange(previousUrl, this.currentUrl); return result; }; const originalReplaceState = history.replaceState; history.replaceState = (state, title, url) => { const previousUrl = this.currentUrl; const result = originalReplaceState.apply(history, [state, title, url]); this.currentUrl = window.location.pathname; void this.handleUrlChange(previousUrl, this.currentUrl); return result; }; } setupPopStateListener() { window.addEventListener('popstate', () => { const previousUrl = this.currentUrl; this.currentUrl = window.location.pathname; void this.handleUrlChange(previousUrl, this.currentUrl); }); } async handleUrlChange(previousUrl, currentUrl, tries = 0) { this.pageReady = false; void new Promise(resolve => setTimeout(resolve, 200)); let success = false; switch (this.currentUrl) { case '/en/reference-book/items': case '/en/warehouse/purchases/edit': case '/reference-book/items': case '/warehouse/purchases/edit': success = await this.addPrintButton(); if (success) { setTimeout(() => { if (document.querySelector('.print') == null) { void this.addPrintButton(); } }, 1000); } break; case '/en/reference-book/items/edit': case '/reference-book/items/edit': success = await this.addPrintButton('.btn-ctrl', true); break; default: success = true; break; } if (success) { this.pageReady = true; } if (!this.pageReady && tries < 5) { setTimeout(() => { void this.handleUrlChange(null, this.currentUrl, tries + 1); }, 600); } } getDataRows() { const dataRows = document.querySelector('.data-rows'); if (dataRows == null) { this.notification.error(i18n('error')); throw new Error('Data rows not found'); } return dataRows; } extractDataFromAngularItemList() { const dataRows = this.getDataRows(); return angular.element(dataRows).controller().grid.data.filter((a) => a._select); } async extractDataFromAngularPurchaseView() { const dataRows = this.getDataRows(); const selectedRows = angular.element(dataRows).controller().data.filter((a) => a._select); const items = []; selectedRows.forEach((row) => { items.push({ name: row.itemName, barcode: row.itemBarcode, code: row.itemCode, id: row.itemId, priceWithVat: row.itemPriceWithVat, priceWithoutVat: row.itemPriceWithoutVat, measurementUnitName: row.measurementUnitName, isActive: true }); }); return items; } extractDataFromAngularItemView() { const form = document.querySelector('ng-form'); if (form == null) { this.notification.error(i18n('error')); return []; } const data = angular.element(form).controller().model; return [data]; } async getViewItems() { let items = []; switch (window.location.pathname) { case '/en/reference-book/items': case '/reference-book/items': items = this.extractDataFromAngularItemList(); break; case '/en/warehouse/purchases/edit': case '/warehouse/purchases/edit': items = await this.extractDataFromAngularPurchaseView(); break; case '/en/reference-book/items/edit': case '/reference-book/items/edit': items = this.extractDataFromAngularItemView(); break; } return items; } async addPrintButton(parentSelector = '.buttons-left', withName = false) { const buttonsLeft = document.querySelector(parentSelector); if (buttonsLeft == null) { return false; } const icon = document.querySelector('i.fa-cloud-upload'); if (icon != null) { const grandParent = icon.parentElement?.parentElement; if (grandParent != null) { grandParent.remove(); } } let printDiv = document.createElement('div'); printDiv.className = 'print'; let button = document.createElement('button'); button.title = i18n('print'); button.type = 'button'; button.className = 'btn btn-sm btn-purple'; let i = document.createElement('i'); i.className = 'fa fa-fw fa-print'; button.appendChild(i); if (withName) { const span = document.createElement('span'); span.className = 'margin-left-5'; span.textContent = i18n('print'); button.appendChild(span); } button.addEventListener('click', () => { void this.printLabels(); }); printDiv.appendChild(button); buttonsLeft.appendChild(printDiv); button = document.createElement('button'); button.title = i18n('weightLabel'); button.type = 'button'; button.className = 'btn btn-sm btn-pink'; i = document.createElement('i'); i.className = 'fa fa-fw fa-balance-scale'; button.appendChild(i); if (withName) { const span = document.createElement('span'); span.className = 'margin-left-5'; span.textContent = i18n('weightLabel'); button.appendChild(span); } button.addEventListener('click', () => { void this.goToWeightLabelModal(); }); printDiv = document.createElement('div'); printDiv.appendChild(button); buttonsLeft.appendChild(printDiv); return true; } async goToWeightLabelModal() { const items = await this.getViewItems(); if (items.length < 1) { this.notification.error(i18n('noItemsSelected')); return; } if (items.length > 1) { this.notification.error(i18n('onlyOneItem')); return; } const item = items[0]; const modal = new WeightLabelModal(this.modal, this.notification); void modal.show(item); } async printLabels() { const items = await this.getViewItems(); if (items.length < 1) { this.notification.error(i18n('noItemsSelected')); return; } void new LabelGenerator(items); } } window.addEventListener('load', () => { void new AdminPrintUserscript(); }); })();