Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## Unreleased
- Add filament label printing with separate presets, QR codes, and filament QR scanning support.
12 changes: 12 additions & 0 deletions client/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,13 @@
},
"qrcode": {
"button": "Print Labels",
"exportButton": "Export Labels",
"printFilamentTitle": "Print Filament Labels",
"printSpoolTitle": "Print Spool Labels",
"title": "Label Printing",
"template": "Label Template",
"templateHelp": "Use {} to insert values of the spool object as text. For example, {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. if a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Lot Nr: {lot_nr}} will only show the label if the spool has a lot number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
"templateHelpFilament": "Use {} to insert values of the filament object as text. For example, {id} will be replaced with the filament id, or {vendor.name} will be replaced with the vendor name. If a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Article: {article_number}} will only show the label if a filament has an article number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
"textSize": "Label Text Size",
"showContent": "Print Label",
"useHTTPUrl": {
Expand All @@ -133,6 +137,14 @@
"selectAll": "Select/Unselect All",
"selectedTotal_one": "{{count}} spool selected",
"selectedTotal_other": "{{count}} spools selected"
},
"filamentSelect": {
"title": "Select Filaments",
"description": "Select filaments to print labels for.",
"noFilamentsSelected": "You have not selected any filaments.",
"selectAll": "Select/Unselect All",
"selectedTotal_one": "{{count}} filament selected",
"selectedTotal_other": "{{count}} filaments selected"
}
},
"scanner": {
Expand Down
1 change: 1 addition & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ function App() {
/>
<Route path="edit/:id" element={<LoadableResourcePage resource="filaments" page="edit" />} />
<Route path="show/:id" element={<LoadableResourcePage resource="filaments" page="show" />} />
<Route path="print" element={<LoadablePage name="filamentPrinting" />} />
</Route>
<Route path="/vendor">
<Route index element={<LoadableResourcePage resource="vendors" page="list" />} />
Expand Down
25 changes: 19 additions & 6 deletions client/src/components/qrCodeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,28 @@ const QRCodeScannerModal = () => {
const result = detectedCodes[0].rawValue;

// Check for the spoolman ID format
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
if (match && match.groups) {
const spoolMatch = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
if (spoolMatch && spoolMatch.groups) {
setVisible(false);
navigate(`/spool/show/${match.groups.id}`);
navigate(`/spool/show/${spoolMatch.groups.id}`);
return;
}
const filamentMatch = result.match(/^web\+spoolman:f-(?<id>[0-9]+)$/i);
if (filamentMatch && filamentMatch.groups) {
setVisible(false);
navigate(`/filament/show/${filamentMatch.groups.id}`);
return;
}
const spoolURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/spool\/show\/(?<id>[0-9]+)$/i);
if (spoolURLmatch && spoolURLmatch.groups) {
setVisible(false);
navigate(`/spool/show/${spoolURLmatch.groups.id}`);
return;
}
const fullURLmatch = result.match(/^https?:\/\/[^/]+\/spool\/show\/(?<id>[0-9]+)$/i);
if (fullURLmatch && fullURLmatch.groups) {
const filamentURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/filament\/show\/(?<id>[0-9]+)$/i);
if (filamentURLmatch && filamentURLmatch.groups) {
setVisible(false);
navigate(`/spool/show/${fullURLmatch.groups.id}`);
navigate(`/filament/show/${filamentURLmatch.groups.id}`);
}
};

Expand Down
71 changes: 71 additions & 0 deletions client/src/pages/filamentPrinting/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PageHeader } from "@refinedev/antd";
import { useTranslate } from "@refinedev/core";
import { theme } from "antd";
import { Content } from "antd/es/layout/layout";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useNavigate, useSearchParams } from "react-router";
import FilamentQRCodePrintingDialog from "../printing/filamentQrCodePrintingDialog";
import FilamentSelectModal from "../printing/filamentSelectModal";

dayjs.extend(utc);

const { useToken } = theme;

export const FilamentPrinting = () => {
const { token } = useToken();
const t = useTranslate();
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();

const filamentIds = searchParams.getAll("filaments").map(Number);
const step = filamentIds.length > 0 ? 1 : 0;

return (
<>
<PageHeader
title={t("printing.qrcode.printFilamentTitle")}
onBack={() => {
const returnUrl = searchParams.get("return");
if (returnUrl) {
navigate(returnUrl, { relative: "path" });
} else {
navigate("/filament");
}
}}
>
<Content
style={{
padding: 20,
minHeight: 280,
margin: "0 auto",
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
color: token.colorText,
fontFamily: token.fontFamily,
fontSize: token.fontSizeLG,
lineHeight: 1.5,
}}
>
{step === 0 && (
<FilamentSelectModal
description={t("printing.filamentSelect.description")}
onPrint={(selectedFilamentIds: number[]) => {
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
newParams.delete("filaments");
selectedFilamentIds.forEach((id) => newParams.append("filaments", id.toString()));
newParams.set("return", "/filament/print");
return newParams;
});
}}
/>
)}
{step === 1 && <FilamentQRCodePrintingDialog filamentIds={filamentIds} />}
</Content>
</PageHeader>
</>
);
};

export default FilamentPrinting;
22 changes: 22 additions & 0 deletions client/src/pages/filaments/functions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useQueries } from "@tanstack/react-query";
import { ExternalFilament } from "../../utils/queryExternalDB";
import { getAPIURL } from "../../utils/url";
import { getOrCreateVendorFromExternal } from "../vendors/functions";
Expand Down Expand Up @@ -48,3 +49,24 @@ export async function createFilamentFromExternal(externalFilament: ExternalFilam
}
return response.json();
}

/**
* Returns an array of queries using the useQueries hook from @tanstack/react-query.
* Each query fetches a filament by its ID from the server.
*
* @param {number[]} ids - An array of filament IDs to fetch.
* @return An array of query results, each containing the fetched filament data.
*/
export function useGetFilamentsByIds(ids: number[]) {
return useQueries({
queries: ids.map((id) => {
return {
queryKey: ["filament", id],
queryFn: async () => {
const res = await fetch(getAPIURL() + "/filament/" + id);
return (await res.json()) as IFilament;
},
};
}),
});
}
18 changes: 17 additions & 1 deletion client/src/pages/filaments/list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import {
EditOutlined,
EyeOutlined,
FileOutlined,
FilterOutlined,
PlusSquareOutlined,
PrinterOutlined,
} from "@ant-design/icons";
import { List, useTable } from "@refinedev/antd";
import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { Button, Dropdown, Table } from "antd";
Expand Down Expand Up @@ -169,6 +176,15 @@ export const FilamentList = () => {
<List
headerButtons={({ defaultButtons }) => (
<>
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={() => {
navigate("print");
}}
>
{t("printing.qrcode.button")}
</Button>
<Button
type="primary"
icon={<FilterOutlined />}
Expand Down
15 changes: 15 additions & 0 deletions client/src/pages/filaments/show.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import { useShow, useTranslate } from "@refinedev/core";
import { PrinterOutlined } from "@ant-design/icons";
import { Button, Typography } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
Expand All @@ -10,6 +11,7 @@ import SpoolIcon from "../../components/spoolIcon";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrencyFormatter } from "../../utils/settings";
import { getBasePath } from "../../utils/url";
import { IFilament } from "./model";
dayjs.extend(utc);

Expand Down Expand Up @@ -65,6 +67,19 @@ export const FilamentShow = () => {
<Button type="primary" onClick={gotoSpools}>
{t("filament.fields.spools")}
</Button>
<Button
type="primary"
icon={<PrinterOutlined />}
href={
getBasePath() +
"/filament/print?filaments=" +
record?.id +
"&return=" +
encodeURIComponent(window.location.pathname)
}
>
{t("printing.qrcode.button")}
</Button>
{defaultButtons}
</>
)}
Expand Down
Loading