feat: mvp for label generation/printing (#274)

* initial label generator for QR codes

* use dynamic URL parameter
This commit is contained in:
Hayden 2023-02-12 15:09:31 -09:00 committed by GitHub
parent c0953bbd26
commit ff75daf6b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 454 additions and 1 deletions

View file

@ -0,0 +1,433 @@
<script setup lang="ts">
definePageMeta({
middleware: ["auth"],
layout: false,
});
useHead({
title: "Homebox | Printer",
});
const bordered = ref(false);
const displayProperties = reactive({
baseURL: window.location.origin,
assetRange: 1,
assetRangeMax: 91,
gapY: 0.25,
columns: 3,
cardHeight: 1,
cardWidth: 2.63,
pageWidth: 8.5,
pageHeight: 11,
pageTopPadding: 0.52,
pageBottomPadding: 0.42,
pageLeftPadding: 0.25,
pageRightPadding: 0.1,
});
type Input = {
page: {
height: number;
width: number;
pageTopPadding: number;
pageBottomPadding: number;
pageLeftPadding: number;
pageRightPadding: number;
};
cardHeight: number;
cardWidth: number;
};
type Output = {
cols: number;
rows: number;
gapY: number;
gapX: number;
card: {
width: number;
height: number;
};
page: {
width: number;
height: number;
pt: number;
pb: number;
pl: number;
pr: number;
};
};
const notifier = useNotifier();
function calculateGridData(input: Input): Output {
const { page, cardHeight, cardWidth } = input;
const availablePageWidth = page.width - page.pageLeftPadding - page.pageRightPadding;
const availablePageHeight = page.height - page.pageTopPadding - page.pageBottomPadding;
if (availablePageWidth < cardWidth || availablePageHeight < cardHeight) {
notifier.error("Page size is too small for the card size");
return out.value;
}
const cols = Math.floor(availablePageWidth / cardWidth);
const rows = Math.floor(availablePageHeight / cardHeight);
const gapX = (availablePageWidth - cols * cardWidth) / (cols - 1);
const gapY = (page.height - rows * cardHeight) / (rows - 1);
return {
cols,
rows,
gapX,
gapY,
card: {
width: cardWidth,
height: cardHeight,
},
page: {
width: page.width,
height: page.height,
pt: page.pageTopPadding,
pb: page.pageBottomPadding,
pl: page.pageLeftPadding,
pr: page.pageRightPadding,
},
};
}
interface InputDef {
label: string;
ref: keyof typeof displayProperties;
type?: "number" | "text";
}
const propertyInputs = computed<InputDef[]>(() => {
return [
{
label: "Asset Start",
ref: "assetRange",
},
{
label: "Asset End",
ref: "assetRangeMax",
},
{
label: "Label Height",
ref: "cardHeight",
},
{
label: "Label Width",
ref: "cardWidth",
},
{
label: "Page Width",
ref: "pageWidth",
},
{
label: "Page Height",
ref: "pageHeight",
},
{
label: "Page Top Padding",
ref: "pageTopPadding",
},
{
label: "Page Bottom Padding",
ref: "pageBottomPadding",
},
{
label: "Page Left Padding",
ref: "pageLeftPadding",
},
{
label: "Page Right Padding",
ref: "pageRightPadding",
},
{
label: "Base URL",
ref: "baseURL",
type: "text",
},
];
});
type LabelData = {
url: string;
name: string;
assetID: string;
location: string;
};
const api = useUserApi();
function fmtAssetID(aid: number | string) {
aid = aid.toString();
let aidStr = aid.toString().padStart(6, "0");
aidStr = aidStr.slice(0, 3) + "-" + aidStr.slice(3);
return aidStr;
}
function getQRCodeUrl(assetID: string): string {
let origin = displayProperties.baseURL.trim();
// remove trailing slash
if (origin.endsWith("/")) {
origin = origin.slice(0, -1);
}
const data = `${origin}/a/${assetID}`;
return `/api/v1/qrcode?data=${encodeURIComponent(data)}&access_token=${api.items.attachmentToken}`;
}
function getItem(n: number): LabelData {
// format n into - seperated string with leading zeros
const assetID = fmtAssetID(n);
return {
url: getQRCodeUrl(assetID),
assetID,
name: "_______________",
location: "_______________",
};
}
const items = computed(() => {
if (displayProperties.assetRange > displayProperties.assetRangeMax) {
return [];
}
const diff = displayProperties.assetRangeMax - displayProperties.assetRange;
if (diff > 999) {
return [];
}
const items: LabelData[] = [];
for (let i = displayProperties.assetRange; i < displayProperties.assetRangeMax; i++) {
items.push(getItem(i));
}
return items;
});
type Row = {
items: LabelData[];
};
type Page = {
rows: Row[];
};
const pages = ref<Page[]>([]);
const out = ref({
cols: 0,
rows: 0,
gapY: 0,
gapX: 0,
card: {
width: 0,
height: 0,
},
page: {
width: 0,
height: 0,
pt: 0,
pb: 0,
pl: 0,
pr: 0,
},
});
function calcPages() {
// Set Out Dimensions
out.value = calculateGridData({
page: {
height: displayProperties.pageHeight,
width: displayProperties.pageWidth,
pageTopPadding: displayProperties.pageTopPadding,
pageBottomPadding: displayProperties.pageBottomPadding,
pageLeftPadding: displayProperties.pageLeftPadding,
pageRightPadding: displayProperties.pageRightPadding,
},
cardHeight: displayProperties.cardHeight,
cardWidth: displayProperties.cardWidth,
});
const calc: Page[] = [];
const perPage = out.value.rows * out.value.cols;
const itemsCopy = [...items.value];
while (itemsCopy.length > 0) {
const page: Page = {
rows: [],
};
for (let i = 0; i < perPage; i++) {
const item = itemsCopy.shift();
if (!item) {
break;
}
if (i % out.value.cols === 0) {
page.rows.push({
items: [],
});
}
page.rows[page.rows.length - 1].items.push(item);
}
calc.push(page);
}
pages.value = calc;
}
onMounted(() => {
calcPages();
});
</script>
<template>
<div class="print:hidden">
<AppToast />
<div class="container max-w-4xl mx-auto p-4 pt-6 prose">
<h1>Homebox Label Generator</h1>
<p>
The Homebox Label Generator is a tool to help you print labels for your Homebox inventory. These are intended to
be print-ahead labels so you can print many labels and have them ready to apply
</p>
<p>
As such, these labels work by printing a URL QR Code and AssetID information on a label. If you've disabled
AssetID's in your Homebox settings, you can still use this tool, but the AssetID's won't reference any item
</p>
<p>
This feature is in early development stages and may change in future releases, if you have feedback please
provide it in the <a href="https://github.com/hay-kot/homebox/discussions/273">GitHub Discussion</a>
</p>
<h2>Tips</h2>
<ul>
<li>
The defaults here are setup for the
<a href="https://www.avery.com/templates/5260">Avery 5260 label sheets</a>. If you're using a different sheet,
you'll need to adjust the settings to match your sheet.
</li>
<li>
If you're customizing your sheet the dimensions are in inches. When building the 5260 sheet, I found that the
dimensions used in their template, did not match what was needed to print within the boxes.
<b>Be prepared for some trial and error</b>
</li>
<li>
When printing be sure to:
<ol>
<li>Set the margins to 0 or None</li>
<li>Set the scaling to 100%</li>
<li>Disable double-sided printing</li>
<li>Print a test page before printing multiple pages</li>
</ol>
</li>
</ul>
<div class="flex gap-2 flex-wrap">
<NuxtLink href="/tools">Tools</NuxtLink>
<NuxtLink href="/home">Home</NuxtLink>
</div>
</div>
<div class="divider max-w-4xl mx-auto"></div>
<div class="container max-w-4xl mx-auto p-4">
<div class="grid grid-cols-2 mx-auto gap-3">
<div v-for="(prop, i) in propertyInputs" :key="i" class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">{{ prop.label }}</span>
</label>
<input
v-model="displayProperties[prop.ref]"
:type="prop.type ? prop.type : 'number'"
step="0.01"
placeholder="Type here"
class="input input-bordered w-full max-w-xs"
/>
</div>
</div>
<div class="max-w-xs">
<div class="form-control">
<label class="cursor-pointer label">
<input v-model="bordered" type="checkbox" class="checkbox checkbox-secondary" />
<span class="label-text">Bordered Labels</span>
</label>
</div>
</div>
<div>
<p>QR Code Example {{ displayProperties.baseURL }}/a/{asset_id}</p>
<BaseButton class="btn-block my-4" @click="calcPages"> Generate Page </BaseButton>
</div>
</div>
</div>
<div class="flex flex-col items-center print-show">
<section
v-for="(page, pi) in pages"
:key="pi"
class="border-2 print:border-none"
:style="{
paddingTop: `${out.page.pt}in`,
paddingBottom: `${out.page.pb}in`,
paddingLeft: `${out.page.pl}in`,
paddingRight: `${out.page.pr}in`,
width: `${out.page.width}in`,
}"
>
<div
v-for="(row, ri) in page.rows"
:key="ri"
class="flex break-inside-avoid"
:style="{
columnGap: `${out.gapX}in`,
rowGap: `${out.gapY}in`,
}"
>
<div
v-for="(item, idx) in row.items"
:key="idx"
class="flex border-2"
:class="{
'border-black': bordered,
'border-transparent': !bordered,
}"
:style="{
height: `${out.card.height}in`,
width: `${out.card.width}in`,
}"
>
<div class="flex items-center">
<img
:src="item.url"
:style="{
width: `${out.card.height * 0.9}in`,
height: `${out.card.height * 0.9}in`,
}"
/>
</div>
<div class="ml-2 flex flex-col justify-center">
<div class="font-bold">{{ item.assetID }}</div>
<div class="text-xs font-light italic">Homebox</div>
<div>{{ item.name }}</div>
<div>{{ item.location }}</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style lang="css">
.letter-size {
width: 8.5in;
height: 11in;
padding: 0.5in;
}
</style>

View file

@ -13,7 +13,7 @@
</template>
</BaseSectionHeader>
<div class="border-t border-gray-300 divide-gray-300 divide-y">
<DetailAction @click="modals.import = true">
<DetailAction @action="modals.import = true">
<template #title>Import Inventory</template>
Imports the standard CSV format for Homebox. This will <b>not</b> overwrite any existing items in your
inventory. It will only add new items.
@ -25,6 +25,26 @@
</div>
</template>
</BaseCard>
<BaseCard>
<template #title>
<BaseSectionHeader>
<Icon name="mdi-file-chart" class="mr-2 -mt-1" />
<span> Reports </span>
<template #description> Generate different reports for your inventory. </template>
</BaseSectionHeader>
<div class="border-t border-gray-300 divide-gray-300 divide-y">
<DetailAction @action="navigateTo('/reports/label-generator')">
<template #title>Asset ID Labels</template>
Generates a printable PDF of labels for a range of Asset ID. These are not specific to your invetory so
your are able to print labels ahead of time and apply them to your inventory when you receive them.
<template #button>
Label Generator
<Icon name="mdi-arrow-right" class="ml-2" />
</template>
</DetailAction>
</div>
</template>
</BaseCard>
<BaseCard>
<template #title>
<BaseSectionHeader>