Lagerort-QR + Zuordnen per Scan
Web: druckbare QR-Etiketten fuer Lagerorte (Locations-Seite) und Route /l/<id> (zeigt den Ort im Browser). Geteilter QR-Helfer (web/src/qr.jsx). iOS: neuer Ablauf "Ort zuordnen" - erst Einzelstueck-QR (…/i/<UID>) scannen, dann Lagerort-QR (…/l/<ID>); das Stueck wird dem Ort zugewiesen, danach gleich das naechste. Plus die zuvor gebaute "Nachschlagen"-Kachel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
90
ios/Sources/AssignScanView.swift
Normal file
90
ios/Sources/AssignScanView.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Zuordnen per Scan: erst ein Einzelstück (…/i/<UID>) scannen, dann einen
|
||||||
|
/// Lagerort (…/l/<ID>) – das Stück wird dem Ort zugewiesen. Danach gleich das
|
||||||
|
/// nächste Stück, ohne Tippen.
|
||||||
|
struct AssignScanView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State private var paused = false
|
||||||
|
@State private var torchOn = false
|
||||||
|
@State private var pending: Item? // gescanntes Stück, wartet auf Ort
|
||||||
|
@State private var status: String?
|
||||||
|
@State private var error: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
ScannerView(onCode: { code in Task { await handle(code) } },
|
||||||
|
isPaused: $paused, torchOn: $torchOn)
|
||||||
|
.aspectRatio(4.0 / 3.0, contentMode: .fit)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
if let pending {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Text("Einzelstück \(pending.uid)").font(.headline)
|
||||||
|
Text("Jetzt den Lagerort-QR scannen")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
Button("Abbrechen") { self.pending = nil }
|
||||||
|
.font(.caption).padding(.top, 2)
|
||||||
|
}
|
||||||
|
.padding().frame(maxWidth: .infinity)
|
||||||
|
.background(Color.accentColor.opacity(0.15))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12)).padding(.horizontal)
|
||||||
|
} else {
|
||||||
|
Text("Schritt 1: Einzelstück-QR scannen")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let status {
|
||||||
|
Text(status).font(.callout).foregroundStyle(.green)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal)
|
||||||
|
}
|
||||||
|
if let error {
|
||||||
|
Text(error).font(.callout).foregroundStyle(.red)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
.navigationTitle("Ort zuordnen")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } }
|
||||||
|
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handle(_ code: String) async {
|
||||||
|
// Lagerort-QR (…/l/<ID>)?
|
||||||
|
if let r = code.range(of: "/l/") {
|
||||||
|
let idStr = String(code[r.upperBound...])
|
||||||
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
|
||||||
|
if let locId = Int(idStr) {
|
||||||
|
guard let it = pending else {
|
||||||
|
error = "Erst ein Einzelstück scannen."; status = nil; return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
_ = try await APIClient.shared.updateItem(id: it.id, ItemUpdateRequest(
|
||||||
|
locationId: locId, shopId: it.shopId,
|
||||||
|
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note))
|
||||||
|
status = "\(it.uid) zugeordnet. Nächstes Stück scannen."
|
||||||
|
error = nil
|
||||||
|
pending = nil
|
||||||
|
} catch { self.error = error.localizedDescription }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Einzelstück-QR (…/i/<UID>)?
|
||||||
|
if let r = code.range(of: "/i/") {
|
||||||
|
let uid = String(code[r.upperBound...])
|
||||||
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
|
||||||
|
if !uid.isEmpty, let it = try? await APIClient.shared.itemByUid(uid: uid) {
|
||||||
|
pending = it; status = nil; error = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error = "Unbekannter Code – bitte einen Einzelstück- oder Lagerort-QR scannen."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ struct RootView: View {
|
|||||||
case .checkin: CheckInView()
|
case .checkin: CheckInView()
|
||||||
case .checkout: CheckOutView()
|
case .checkout: CheckOutView()
|
||||||
case .lookup: LookupScanView()
|
case .lookup: LookupScanView()
|
||||||
|
case .assign: AssignScanView()
|
||||||
case .expiring:
|
case .expiring:
|
||||||
ExpiringView()
|
ExpiringView()
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -111,6 +112,13 @@ struct ScanTabView: View {
|
|||||||
subtitle: "Ohne Barcode – Name, Kategorie, eigene Felder",
|
subtitle: "Ohne Barcode – Name, Kategorie, eigene Felder",
|
||||||
systemImage: "plus")
|
systemImage: "plus")
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
router.route = .assign
|
||||||
|
} label: {
|
||||||
|
ActionTile(title: "Ort zuordnen",
|
||||||
|
subtitle: "Einzelstück-QR scannen, dann Lagerort-QR",
|
||||||
|
systemImage: "mappin.and.ellipse")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ enum Route: String {
|
|||||||
case checkout
|
case checkout
|
||||||
case expiring
|
case expiring
|
||||||
case lookup
|
case lookup
|
||||||
|
case assign
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Router: ObservableObject {
|
final class Router: ObservableObject {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
4C486D190D1AB338925D29B5 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */; };
|
4C486D190D1AB338925D29B5 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */; };
|
||||||
4F86620DF8303DB02314DB5A /* CheckInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0365A16FEAE3F2BEC321E68A /* CheckInView.swift */; };
|
4F86620DF8303DB02314DB5A /* CheckInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0365A16FEAE3F2BEC321E68A /* CheckInView.swift */; };
|
||||||
56A5DFAC86DF972B1481A691 /* ItemViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B59B9F69BC30451D45A3C7 /* ItemViews.swift */; };
|
56A5DFAC86DF972B1481A691 /* ItemViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B59B9F69BC30451D45A3C7 /* ItemViews.swift */; };
|
||||||
|
5A0C3254B45CA13B1C3D9265 /* AssignScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76393ADA26304E5BC5AD7E7F /* AssignScanView.swift */; };
|
||||||
6816C33381DF52A96E5BB303 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314B1BB7220691A7423E8929 /* Models.swift */; };
|
6816C33381DF52A96E5BB303 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314B1BB7220691A7423E8929 /* Models.swift */; };
|
||||||
728CF5124D2D5BBF826BCD2C /* ServerFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1847E65D41ACEDD01CD108BC /* ServerFetch.swift */; };
|
728CF5124D2D5BBF826BCD2C /* ServerFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1847E65D41ACEDD01CD108BC /* ServerFetch.swift */; };
|
||||||
75F805F8AE93479933BD811D /* VorraniaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9948219CDDC4188EA4298F22 /* VorraniaApp.swift */; };
|
75F805F8AE93479933BD811D /* VorraniaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9948219CDDC4188EA4298F22 /* VorraniaApp.swift */; };
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
||||||
6F574168AA0F849D46C384EE /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = "<group>"; };
|
6F574168AA0F849D46C384EE /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = "<group>"; };
|
||||||
717C8EB336170526F5F3E695 /* DateScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateScanView.swift; sourceTree = "<group>"; };
|
717C8EB336170526F5F3E695 /* DateScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateScanView.swift; sourceTree = "<group>"; };
|
||||||
|
76393ADA26304E5BC5AD7E7F /* AssignScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignScanView.swift; sourceTree = "<group>"; };
|
||||||
7B5BB49FBEFF88768B0BC902 /* ObjectStockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectStockView.swift; sourceTree = "<group>"; };
|
7B5BB49FBEFF88768B0BC902 /* ObjectStockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectStockView.swift; sourceTree = "<group>"; };
|
||||||
7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||||
8AF19A735AC17451B57BF5BB /* ScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerView.swift; sourceTree = "<group>"; };
|
8AF19A735AC17451B57BF5BB /* ScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerView.swift; sourceTree = "<group>"; };
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
children = (
|
children = (
|
||||||
378F1B2DE567B62169E84426 /* APIClient.swift */,
|
378F1B2DE567B62169E84426 /* APIClient.swift */,
|
||||||
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */,
|
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */,
|
||||||
|
76393ADA26304E5BC5AD7E7F /* AssignScanView.swift */,
|
||||||
AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */,
|
AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */,
|
||||||
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */,
|
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */,
|
||||||
E580BA2815BB8F6341D64B47 /* ChartCards.swift */,
|
E580BA2815BB8F6341D64B47 /* ChartCards.swift */,
|
||||||
@@ -215,6 +218,7 @@
|
|||||||
files = (
|
files = (
|
||||||
39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */,
|
39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */,
|
||||||
F3F718BFC60BF945CFA32BD2 /* AppDashboards.swift in Sources */,
|
F3F718BFC60BF945CFA32BD2 /* AppDashboards.swift in Sources */,
|
||||||
|
5A0C3254B45CA13B1C3D9265 /* AssignScanView.swift in Sources */,
|
||||||
A7017E6C0532A650439AED7B /* BestBeforeText.swift in Sources */,
|
A7017E6C0532A650439AED7B /* BestBeforeText.swift in Sources */,
|
||||||
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */,
|
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */,
|
||||||
FFD4DB783F89083C9098F1C4 /* ChartCards.swift in Sources */,
|
FFD4DB783F89083C9098F1C4 /* ChartCards.swift in Sources */,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Groups from "./pages/Groups";
|
|||||||
import Categories from "./pages/Categories";
|
import Categories from "./pages/Categories";
|
||||||
import Shops from "./pages/Shops";
|
import Shops from "./pages/Shops";
|
||||||
import ItemResolve from "./pages/ItemResolve";
|
import ItemResolve from "./pages/ItemResolve";
|
||||||
|
import LocationResolve from "./pages/LocationResolve";
|
||||||
import Locations from "./pages/Locations";
|
import Locations from "./pages/Locations";
|
||||||
import PackageTypes from "./pages/PackageTypes";
|
import PackageTypes from "./pages/PackageTypes";
|
||||||
import Units from "./pages/Units";
|
import Units from "./pages/Units";
|
||||||
@@ -136,6 +137,7 @@ export default function App() {
|
|||||||
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
|
<Route path="/products/new" element={<Protected adminOnly><ProductForm /></Protected>} />
|
||||||
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
|
<Route path="/products/:id" element={<Protected><ProductForm /></Protected>} />
|
||||||
<Route path="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
<Route path="/i/:uid" element={<Protected><ItemResolve /></Protected>} />
|
||||||
|
<Route path="/l/:id" element={<Protected><LocationResolve /></Protected>} />
|
||||||
<Route path="/groups" element={<Protected><Groups /></Protected>} />
|
<Route path="/groups" element={<Protected><Groups /></Protected>} />
|
||||||
<Route path="/categories" element={<Protected><Categories /></Protected>} />
|
<Route path="/categories" element={<Protected><Categories /></Protected>} />
|
||||||
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
|
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
|
||||||
|
|||||||
38
web/src/pages/LocationResolve.jsx
Normal file
38
web/src/pages/LocationResolve.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { api } from "../api";
|
||||||
|
import Icon from "../components/Icon";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ziel eines gescannten Lagerort-QR (/l/<id>). Im Browser zeigt es nur den
|
||||||
|
* Namen; das eigentliche Zuordnen (Stück → Ort) läuft über die App.
|
||||||
|
*/
|
||||||
|
export default function LocationResolve() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [name, setName] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.listLocations()
|
||||||
|
.then((ls) => {
|
||||||
|
const l = ls.find((x) => String(x.id) === String(id));
|
||||||
|
if (l) setName(l.name);
|
||||||
|
else setError("Lagerort nicht gefunden.");
|
||||||
|
})
|
||||||
|
.catch((e) => setError(e.message));
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="page-head">
|
||||||
|
<div>
|
||||||
|
<h1>Lagerort {name || `#${id}`}</h1>
|
||||||
|
<div className="sub">
|
||||||
|
{error || "Diesen QR-Code in der App scannen (Ort zuordnen), um Einzelstücke hier einzuordnen."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
|
import { QrImg, printQrLabels } from "../qr";
|
||||||
|
|
||||||
export default function Locations() {
|
export default function Locations() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -71,6 +72,12 @@ export default function Locations() {
|
|||||||
<h1>Lagerorte</h1>
|
<h1>Lagerorte</h1>
|
||||||
<div className="sub">Orte lassen sich verschachteln (z.B. Keller → Regal 2 → Fach A)</div>
|
<div className="sub">Orte lassen sich verschachteln (z.B. Keller → Regal 2 → Fach A)</div>
|
||||||
</div>
|
</div>
|
||||||
|
{locations.length > 0 && (
|
||||||
|
<button className="btn" onClick={() => printQrLabels("Lagerort-Etiketten",
|
||||||
|
locations.map((l) => ({ text: `${window.location.origin}/l/${l.id}`, line1: l.name, line2: "Lagerort" })))}>
|
||||||
|
<Icon name="download" size={16} />QR-Etiketten drucken
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
|
||||||
@@ -108,9 +115,12 @@ export default function Locations() {
|
|||||||
<span className="badge">in {nameById[l.parent_id]}</span>
|
<span className="badge">in {nameById[l.parent_id]}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<QrImg text={`${window.location.origin}/l/${l.id}`} size={44} />
|
||||||
<button className="btn-icon danger" onClick={() => remove(l.id)} title="Löschen">
|
<button className="btn-icon danger" onClick={() => remove(l.id)} title="Löschen">
|
||||||
<Icon name="trash" size={16} />
|
<Icon name="trash" size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{locations.length === 0 && <li className="empty">Noch keine Lagerorte.</li>}
|
{locations.length === 0 && <li className="empty">Noch keine Lagerorte.</li>}
|
||||||
|
|||||||
42
web/src/qr.jsx
Normal file
42
web/src/qr.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
/** Kleines QR-Bild für einen Wert (asynchron erzeugt). */
|
||||||
|
export function QrImg({ text, size = 60 }) {
|
||||||
|
const [url, setUrl] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
let ok = true;
|
||||||
|
QRCode.toDataURL(text, { margin: 1, width: size * 2 })
|
||||||
|
.then((u) => { if (ok) setUrl(u); })
|
||||||
|
.catch(() => {});
|
||||||
|
return () => { ok = false; };
|
||||||
|
}, [text, size]);
|
||||||
|
return url
|
||||||
|
? <img src={url} width={size} height={size} alt="QR" style={{ display: "block" }} />
|
||||||
|
: <span style={{ display: "inline-block", width: size, height: size }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esc = (s) => (s || "").replace(/[<>&]/g, "");
|
||||||
|
|
||||||
|
/** Druckt QR-Etiketten in einem neuen Fenster. entries: [{ text, line1, line2 }]. */
|
||||||
|
export async function printQrLabels(title, entries) {
|
||||||
|
const labels = await Promise.all(entries.map(async (e) => {
|
||||||
|
const url = await QRCode.toDataURL(e.text, { margin: 1, width: 260 });
|
||||||
|
return `<div class="label"><img src="${url}"/><div class="l1">${esc(e.line1)}</div>`
|
||||||
|
+ `<div class="l2">${esc(e.line2)}</div></div>`;
|
||||||
|
}));
|
||||||
|
const w = window.open("", "_blank");
|
||||||
|
if (!w) return false;
|
||||||
|
w.document.write(
|
||||||
|
`<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title><style>
|
||||||
|
body{font-family:sans-serif;margin:8mm;display:flex;flex-wrap:wrap;gap:6mm}
|
||||||
|
.label{width:34mm;text-align:center;border:1px solid #ccc;border-radius:3mm;padding:3mm;page-break-inside:avoid}
|
||||||
|
.label img{width:28mm;height:28mm}
|
||||||
|
.l1{font-weight:700;font-size:10pt;margin-top:1mm}
|
||||||
|
.l2{font-size:8pt;color:#444}
|
||||||
|
</style></head><body>${labels.join("")}
|
||||||
|
<script>window.onload=function(){window.print()}</script></body></html>`,
|
||||||
|
);
|
||||||
|
w.document.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user