Label printing page updates

No longer a modal
Use flex divs instead of a table
This commit is contained in:
Donkie
2024-08-04 23:23:13 +02:00
parent 9aa9479a14
commit 9318ef5c19
9 changed files with 154 additions and 173 deletions

View File

@@ -0,0 +1,50 @@
import React from "react";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Content } from "antd/es/layout/layout";
import { theme } from "antd";
import SpoolQRCodePrintingDialog from "./spoolQrCodePrintingDialog";
import SpoolSelectModal from "./spoolSelectModal";
import { ISpool } from "../spools/model";
dayjs.extend(utc);
const { useToken } = theme;
export const Printing: React.FC<IResourceComponentsProps> = () => {
const { token } = useToken();
const t = useTranslate();
const [step, setStep] = React.useState<0 | 1>(0);
const [selectedSpools, setSelectedSpools] = React.useState<ISpool[]>([]);
return (
<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 && (
<SpoolSelectModal
description={t("printing.spoolSelect.description")}
onContinue={(spools) => {
setSelectedSpools(spools);
setStep(1);
}}
/>
)}
{step === 1 && <SpoolQRCodePrintingDialog items={selectedSpools} />}
</Content>
);
};
export default Printing;

View File

@@ -0,0 +1,103 @@
import React, { ReactNode } from "react";
import { ISpool } from "../spools/model";
import { useGetSetting, useSetSetting } from "../../utils/querySettings";
import { v4 as uuidv4 } from "uuid";
export interface PrintSettings {
id: string;
name?: string;
margin?: { top: number; bottom: number; left: number; right: number };
printerMargin?: { top: number; bottom: number; left: number; right: number };
spacing?: { horizontal: number; vertical: number };
columns?: number;
rows?: number;
skipItems?: number;
itemCopies?: number;
paperSize?: string;
customPaperSize?: { width: number; height: number };
borderShowMode?: "none" | "border" | "grid";
}
export interface QRCodePrintSettings {
showContent?: boolean;
textSize?: number;
showSpoolmanIcon?: boolean;
printSettings: PrintSettings;
}
export interface SpoolQRCodePrintSettings {
template?: string;
labelSettings: QRCodePrintSettings;
}
export function useGetPrintSettings(): SpoolQRCodePrintSettings[] | undefined {
const { data } = useGetSetting("print_settings");
if (!data) return;
const parsed: SpoolQRCodePrintSettings[] =
data && data.value ? JSON.parse(data.value) : ([] as SpoolQRCodePrintSettings[]);
// Loop through all parsed and generate a new ID field if it's not set
return parsed.map((settings) => {
if (!settings.labelSettings.printSettings.id) {
settings.labelSettings.printSettings.id = uuidv4();
}
return settings;
});
}
export function useSetPrintSettings(): (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => void {
const mut = useSetSetting("print_settings");
return (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => {
mut.mutate(spoolQRCodePrintSettings);
};
}
interface GenericObject {
[key: string]: any;
extra: { [key: string]: string };
}
function getTagValue(tag: string, obj: GenericObject): any {
// Split tag by .
const tagParts = tag.split(".");
if (tagParts[0] === "extra") {
return JSON.parse(obj.extra[tagParts[1]]);
}
const value = obj[tagParts[0]] ?? "?";
// check if value is itself an object. If so, recursively call this and remove the first part of the tag
if (typeof value === "object") {
return getTagValue(tagParts.slice(1).join("."), value);
}
return value;
}
function applyNewline(text: string): JSX.Element[] {
return text.split("\n").map((line, idx, arr) => (
<span key={idx}>
{line}
{idx < arr.length - 1 && <br />}
</span>
));
}
function applyTextFormatting(text: string): JSX.Element[] {
const regex = /\*\*([\w\W]*?)\*\*/g;
const parts = text.split(regex);
// Map over the parts and wrap matched text with <b> tags
const elements = parts.map((part, index) => {
// Even index: outside asterisks, odd index: inside asterisks (to be bolded)
const node = applyNewline(part);
return index % 2 === 0 ? <span key={index}>{node}</span> : <b key={index}>{node}</b>;
});
return elements;
}
export function renderLabelContents(template: string, spool: ISpool): JSX.Element {
// Find all {tags} in the template string and loop over them
let result = template.replace(/\{(.*?)\}/g, function (_, tag) {
return getTagValue(tag, spool);
});
// Split string on \n into individual lines
return <>{applyTextFormatting(result)}</>;
}

View File

@@ -0,0 +1,788 @@
import React, { useRef } from "react";
import {
Modal,
Slider,
Button,
Select,
Row,
Col,
Form,
Divider,
RadioChangeEvent,
Radio,
InputNumber,
Collapse,
} from "antd";
import ReactToPrint from "react-to-print";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
import { PrintSettings } from "./printing";
interface PrintingDialogProps {
items: JSX.Element[];
printSettings: PrintSettings;
setPrintSettings: (setPrintSettings: PrintSettings) => void;
style?: string;
extraSettings?: JSX.Element;
extraSettingsStart?: JSX.Element;
}
interface PaperDimensions {
width: number;
height: number;
}
const paperDimensions: { [key: string]: PaperDimensions } = {
A3: {
width: 297,
height: 420,
},
A4: {
width: 210,
height: 297,
},
A5: {
width: 148,
height: 210,
},
Letter: {
width: 216,
height: 279,
},
Legal: {
width: 216,
height: 356,
},
Tabloid: {
width: 279,
height: 432,
},
};
const PrintingDialog: React.FC<PrintingDialogProps> = ({
items,
printSettings,
setPrintSettings,
style,
extraSettings,
extraSettingsStart,
}) => {
const t = useTranslate();
const [collapseState, setCollapseState] = useSavedState<string[]>("print-collapseState", []);
const [previewScale, setPreviewScale] = useSavedState("print-previewScale", 0.6);
const margin = printSettings?.margin || { top: 10, bottom: 10, left: 10, right: 10 };
const printerMargin = printSettings?.printerMargin || { top: 5, bottom: 5, left: 5, right: 5 };
const spacing = printSettings?.spacing || { horizontal: 0, vertical: 0 };
const paperColumns = printSettings?.columns || 3;
const paperRows = printSettings?.rows || 8;
const skipItems = printSettings?.skipItems || 0;
const itemCopies = printSettings?.itemCopies || 1;
const paperSize = printSettings?.paperSize || "A4";
const customPaperSize = printSettings?.customPaperSize || { width: 210, height: 297 };
const borderShowMode = printSettings?.borderShowMode || "grid";
const paperWidth = paperSize === "custom" ? customPaperSize.width : paperDimensions[paperSize].width;
const paperHeight = paperSize === "custom" ? customPaperSize.height : paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const itemWidth = (paperWidth - margin.left - margin.right - spacing.horizontal) / paperColumns - spacing.horizontal;
const itemHeight = (paperHeight - margin.top - margin.bottom - spacing.vertical) / paperRows - spacing.vertical;
const itemsPerRow = paperColumns;
const itemsPerPage = itemsPerRow * paperRows;
const itemsIncludingSkipped = [...Array(skipItems).fill(<></>)];
for (const item of items) {
for (let i = 0; i < itemCopies; i += 1) {
itemsIncludingSkipped.push(item);
}
}
const pageBlocks = [];
for (let page_idx = 0; page_idx < itemsIncludingSkipped.length / itemsPerPage; page_idx += 1) {
pageBlocks.push(itemsIncludingSkipped.slice(page_idx * itemsPerPage, (page_idx + 1) * itemsPerPage));
}
const pages = pageBlocks.map(function (items, pageIdx) {
const itemDivs = items.map((item, itemIdx) => {
const isFirstColumn = itemIdx % itemsPerRow === 0;
const isLastColumn = (itemIdx + 1) % itemsPerRow === 0;
const isFirstRow = itemIdx < itemsPerRow;
const isLastRow = itemsPerPage - itemIdx <= itemsPerRow;
return (
<div
key={itemIdx}
style={{
width: `${itemWidth}mm`,
height: `${itemHeight}mm`,
border: borderShowMode === "grid" ? "1px solid #000" : "none",
paddingLeft: isFirstColumn ? `${Math.max(printerMargin.left - margin.left, 0)}mm` : 0,
paddingRight: isLastColumn ? `${Math.max(printerMargin.right - margin.right, 0)}mm` : 0,
paddingTop: isFirstRow ? `${Math.max(printerMargin.top - margin.top, 0)}mm` : 0,
paddingBottom: isLastRow ? `${Math.max(printerMargin.bottom - margin.bottom, 0)}mm` : 0,
}}
>
{item}
</div>
);
});
return (
<div
className="print-page"
key={pageIdx}
style={{
width: `${paperWidth}mm`,
height: `${paperHeight}mm`,
backgroundColor: "#FFF",
overflow: "hidden",
}}
>
<div
className="print-page-area"
style={{
outline: borderShowMode !== "none" ? "1px solid #000" : "none",
outlineOffset: "-1px",
height: `${paperHeight - margin.top - margin.bottom}mm`,
width: `${paperWidth - margin.left - margin.right}mm`,
marginTop: `calc(${margin.top}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginLeft: `calc(${margin.left}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginRight: `calc(${margin.right}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginBottom: `calc(${margin.bottom}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
}}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
rowGap: `${spacing.vertical}mm`,
columnGap: `${spacing.horizontal}mm`,
paddingTop: `${spacing.vertical}mm`,
paddingLeft: `${spacing.horizontal}mm`,
}}
>
{itemDivs}
</div>
</div>
</div>
);
});
return (
<>
<Row gutter={16}>
<Col span={14}>
<div
style={{
transform: "translateZ(0)",
overflow: "hidden scroll",
height: "70vh",
}}
>
<div
className="print-container"
ref={printRef}
style={{
transform: `scale(${previewScale})`,
transformOrigin: "top left",
}}
>
<style>
{`
@media print {
html, body {
height: initial !important;
overflow: initial !important;
-webkit-print-color-adjust: exact;
}
@page {
size: auto;
margin: 0;
}
.print-container {
transform: scale(1) !important;
}
.print-page {
page-break-before: auto;
}
}
@media screen {
.print-page {
margin-top: 10mm;
}
}
.print-page {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
line-height: 1.2;
}
.print-page * {
box-sizing: border-box;
}
${style ?? ""}
`}
</style>
{pages}
</div>
</div>
</Col>
<Col span={10}>
<Form labelAlign="left" colon={false} labelWrap={true} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
{extraSettingsStart}
<Divider />
<Form.Item label={t("printing.generic.skipItems")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={30}
value={skipItems}
onChange={(value) => {
printSettings.skipItems = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={skipItems}
onChange={(value) => {
printSettings.skipItems = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.itemCopies")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={3}
value={itemCopies}
onChange={(value) => {
printSettings.itemCopies = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={itemCopies}
onChange={(value) => {
printSettings.itemCopies = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.showBorder")}>
<Radio.Group
options={[
{ label: t("printing.generic.borders.none"), value: "none" },
{
label: t("printing.generic.borders.border"),
value: "border",
},
{ label: t("printing.generic.borders.grid"), value: "grid" },
]}
onChange={(e: RadioChangeEvent) => {
printSettings.borderShowMode = e.target.value;
setPrintSettings(printSettings);
}}
value={borderShowMode}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t("printing.generic.previewScale")}>
<Row>
<Col span={12}>
<Slider
min={0.1}
max={1}
step={0.01}
value={previewScale}
onChange={(value) => {
setPreviewScale(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0.1}
max={1}
step={0.01}
style={{ margin: "0 16px" }}
value={previewScale}
onChange={(value) => {
setPreviewScale(value ?? 0.1);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<Collapse
defaultActiveKey={collapseState}
bordered={false}
ghost
onChange={(key) => {
if (Array.isArray(key)) {
setCollapseState(key);
}
}}
>
<Collapse.Panel header={t("printing.generic.contentSettings")} key="1">
{extraSettings}
</Collapse.Panel>
<Collapse.Panel header={t("printing.generic.layoutSettings")} key="2">
{t("printing.generic.description")}
<Divider />
<Form.Item label={t("printing.generic.paperSize")}>
<Select
value={paperSize}
onChange={(value) => {
printSettings.paperSize = value;
setPrintSettings(printSettings);
}}
>
{Object.keys(paperDimensions).map((key) => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
<Select.Option value="custom">{t("printing.generic.customSize")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label={t("printing.generic.dimensions")} hidden={paperSize !== "custom"}>
<Row align="middle">
<Col span={11}>
<InputNumber
value={customPaperSize.width}
min={0.1}
addonAfter="mm"
onChange={(value) => {
customPaperSize.width = value ?? 0;
printSettings.customPaperSize = customPaperSize;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={2} style={{ textAlign: "center" }}>
x
</Col>
<Col span={11}>
<InputNumber
value={customPaperSize.height}
min={0.1}
addonAfter="mm"
onChange={(value) => {
customPaperSize.height = value ?? 0;
printSettings.customPaperSize = customPaperSize;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.columns")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={5}
value={paperColumns}
onChange={(value) => {
printSettings.columns = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperColumns}
onChange={(value) => {
printSettings.columns = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.rows")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={15}
value={paperRows}
onChange={(value) => {
printSettings.rows = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperRows}
onChange={(value) => {
printSettings.rows = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<p>{t("printing.generic.helpMargin")}</p>
<Form.Item label={t("printing.generic.marginLeft")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={margin.left}
onChange={(value) => {
margin.left = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={margin.left}
addonAfter="mm"
onChange={(value) => {
margin.left = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginTop")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={margin.top}
onChange={(value) => {
margin.top = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={margin.top}
addonAfter="mm"
onChange={(value) => {
margin.top = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginRight")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={margin.right}
onChange={(value) => {
margin.right = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={margin.right}
addonAfter="mm"
onChange={(value) => {
margin.right = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginBottom")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={margin.bottom}
onChange={(value) => {
margin.bottom = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={margin.bottom}
addonAfter="mm"
onChange={(value) => {
margin.bottom = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<p>{t("printing.generic.helpPrinterMargin")}</p>
<Form.Item label={t("printing.generic.printerMarginLeft")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMargin.left}
onChange={(value) => {
printerMargin.left = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMargin.left}
addonAfter="mm"
onChange={(value) => {
printerMargin.left = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginTop")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMargin.top}
onChange={(value) => {
printerMargin.top = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMargin.top}
addonAfter="mm"
onChange={(value) => {
printerMargin.top = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginRight")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMargin.right}
onChange={(value) => {
printerMargin.right = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMargin.right}
addonAfter="mm"
onChange={(value) => {
printerMargin.right = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginBottom")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMargin.bottom}
onChange={(value) => {
printerMargin.bottom = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMargin.bottom}
addonAfter="mm"
onChange={(value) => {
printerMargin.bottom = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<Form.Item label={t("printing.generic.horizontalSpacing")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={spacing.horizontal}
onChange={(value) => {
spacing.horizontal = value;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={spacing.horizontal}
addonAfter="mm"
onChange={(value) => {
spacing.horizontal = value ?? 0;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.verticalSpacing")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={spacing.vertical}
onChange={(value) => {
spacing.vertical = value;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={spacing.vertical}
addonAfter="mm"
onChange={(value) => {
spacing.vertical = value ?? 0;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
</Collapse.Panel>
</Collapse>
</Form>
</Col>
</Row>
<Row justify={"end"}>
<Col>
<ReactToPrint
key="print-button"
trigger={() => <Button type="primary">{t("printing.generic.print")}</Button>}
content={() => printRef.current}
/>
</Col>
</Row>
</>
);
};
export default PrintingDialog;

View File

@@ -0,0 +1,155 @@
import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd";
import { useSavedState } from "../../utils/saveload";
import PrintingDialog from "./printingDialog";
import { useTranslate } from "@refinedev/core";
import { QRCodePrintSettings } from "./printing";
interface QRCodeData {
value: string;
label?: JSX.Element;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
items: QRCodeData[];
printSettings: QRCodePrintSettings;
setPrintSettings: (setPrintSettings: QRCodePrintSettings) => void;
extraSettings?: JSX.Element;
extraSettingsStart?: JSX.Element;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
items,
printSettings,
setPrintSettings,
extraSettings,
extraSettingsStart,
}) => {
const t = useTranslate();
const showContent = printSettings?.showContent === undefined ? true : printSettings?.showContent;
const textSize = printSettings?.textSize || 3;
const showSpoolmanIcon = printSettings?.showSpoolmanIcon === undefined ? true : printSettings?.showSpoolmanIcon;
const elements = items.map((item) => {
return (
<div className="print-qrcode-item">
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
</div>
{showContent && <div className="print-qrcode-title">{item.label ?? item.value}</div>}
</div>
);
});
return (
<PrintingDialog
items={elements}
printSettings={printSettings.printSettings}
setPrintSettings={(newSettings) => {
printSettings.printSettings = newSettings;
setPrintSettings(printSettings);
}}
extraSettingsStart={extraSettingsStart}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch
checked={showContent}
onChange={(checked) => {
printSettings.showContent = checked;
setPrintSettings(printSettings);
}}
/>
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={2}
max={7}
value={textSize}
step={0.1}
onChange={(value) => {
printSettings.textSize = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
printSettings.textSize = value ?? 5;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolmanIcon")}>
<Switch
checked={showSpoolmanIcon}
onChange={(checked) => {
printSettings.showSpoolmanIcon = checked;
setPrintSettings(printSettings);
}}
/>
</Form.Item>
{extraSettings}
</>
}
style={`
.print-page .print-qrcode-item {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
}
.print-page .print-qrcode-container {
max-width: ${showContent ? "50%" : "100%"};
display: flex;
}
.print-page .print-qrcode {
width: auto !important;
height: auto !important;
padding: 2mm;
}
.print-page .print-qrcode-title {
flex: 1 1 auto;
font-size: ${textSize}mm;
color: #000;
overflow: hidden;
}
.print-page canvas, .print-page svg {
/* display: block; */
object-fit: contain;
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
`}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -0,0 +1,319 @@
import { Button, Flex, Form, Input, Modal, Popconfirm, Select, Space, Switch, Table, Typography } from "antd";
import { IFilament } from "../filaments/model";
import { ISpool } from "../spools/model";
import QRCodePrintingDialog from "./qrCodePrintingDialog";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
import {
QRCodePrintSettings,
SpoolQRCodePrintSettings,
renderLabelContents,
useGetPrintSettings,
useSetPrintSettings,
} from "./printing";
import { useMemo, useState } from "react";
import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons";
import { v4 as uuidv4 } from "uuid";
import _ from "lodash";
import TextArea from "antd/es/input/TextArea";
import { EntityType, useGetFields } from "../../utils/queryFields";
const { Text } = Typography;
interface SpoolQRCodePrintingDialog {
items: ISpool[];
}
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ items }) => {
const t = useTranslate();
// Selected setting state
const [selectedSetting, setSelectedSetting] = useState<string | undefined>();
// Keep a local copy of the settings which is what's actually displayed. Use the remote state only for saving.
// This decouples the debounce stuff from the UI
const [localSettings, setLocalSettings] = useState<SpoolQRCodePrintSettings[] | undefined>();
const remoteSettings = useGetPrintSettings();
const setRemoteSettings = useSetPrintSettings();
const debouncedSetRemoteSettings = useMemo(() => _.debounce(setRemoteSettings, 500), []);
const allPrintSettings = localSettings ?? remoteSettings;
const setPrintSettings = (newSettings: SpoolQRCodePrintSettings[]) => {
setLocalSettings(newSettings);
debouncedSetRemoteSettings(newSettings);
};
// Functions to update settings
const addNewPrintSettings = () => {
if (!allPrintSettings) return;
const newId = uuidv4();
const newSetting = {
labelSettings: {
printSettings: {
id: newId,
name: t("printing.generic.newSetting"),
},
},
};
setPrintSettings([...allPrintSettings, newSetting]);
setSelectedSetting(newId);
return newSetting;
};
const updateCurrentPrintSettings = (newSettings: SpoolQRCodePrintSettings) => {
if (!allPrintSettings) return;
setPrintSettings(
allPrintSettings.map((settings) =>
settings.labelSettings.printSettings.id === newSettings.labelSettings.printSettings.id ? newSettings : settings
)
);
};
const deleteCurrentPrintSettings = () => {
if (!allPrintSettings) return;
setPrintSettings(
allPrintSettings.filter((qSetting) => qSetting.labelSettings.printSettings.id !== selectedSetting)
);
setSelectedSetting(undefined);
};
// Initialize settings
let selectedPrintSetting: SpoolQRCodePrintSettings;
if (allPrintSettings === undefined) {
// DB not loaded yet, use a temporary one
selectedPrintSetting = {
labelSettings: {
printSettings: {
id: "TEMP",
name: t("printing.generic.newSetting"),
},
},
};
} else {
// DB is loaded, find the selected setting
if (allPrintSettings.length === 0) {
// DB loaded, but no settings found, add a new one and select it
const newSetting = addNewPrintSettings();
if (!newSetting) {
console.error("Error adding new setting, this should never happen");
return;
}
// Mutate the allPrintSettings list so that the rest of the UI will work fine
allPrintSettings.push(newSetting);
selectedPrintSetting = newSetting;
} else {
// DB loaded and at least 1 setting exists
if (!selectedSetting) {
// No setting has been selected, select the first one
selectedPrintSetting = allPrintSettings[0];
setSelectedSetting(allPrintSettings[0].labelSettings.printSettings.id);
} else {
// A setting has been selected, find it
const foundSetting = allPrintSettings.find(
(settings) => settings.labelSettings.printSettings.id === selectedSetting
);
if (foundSetting) {
selectedPrintSetting = foundSetting;
} else {
// Selected setting not found, select a temp one
selectedPrintSetting = {
labelSettings: {
printSettings: {
id: "TEMP",
name: t("printing.generic.newSetting"),
},
},
};
}
}
}
}
const [templateHelpOpen, setTemplateHelpOpen] = useState(false);
const template =
selectedPrintSetting.template ??
`**{filament.vendor.name} - {filament.name}
#{id} - {filament.material}**
Spool Weight: {filament.spool_weight} g
ET: {filament.settings_extruder_temp} °C
BT: {filament.settings_bed_temp} °C
Lot Nr: {lot_nr}
{comment}
{filament.comment}
{filament.vendor.comment}`;
const spoolTags = [
{ tag: "id" },
{ tag: "registered" },
{ tag: "first_used" },
{ tag: "last_used" },
{ tag: "price" },
{ tag: "initial_weight" },
{ tag: "spool_weight" },
{ tag: "remaining_weight" },
{ tag: "used_weight" },
{ tag: "remaining_length" },
{ tag: "used_length" },
{ tag: "location" },
{ tag: "lot_nr" },
{ tag: "comment" },
{ tag: "archived" },
];
const spoolFields = useGetFields(EntityType.spool);
if (spoolFields.data !== undefined) {
spoolFields.data.forEach((field) => {
spoolTags.push({ tag: `extra.${field.key}` });
});
}
const filamentTags = [
{ tag: "filament.id" },
{ tag: "filament.registered" },
{ tag: "filament.name" },
{ tag: "filament.material" },
{ tag: "filament.price" },
{ tag: "filament.density" },
{ tag: "filament.diameter" },
{ tag: "filament.weight" },
{ tag: "filament.spool_weight" },
{ tag: "filament.article_number" },
{ tag: "filament.comment" },
{ tag: "filament.settings_extruder_temp" },
{ tag: "filament.settings_bed_temp" },
{ tag: "filament.color_hex" },
{ tag: "filament.multi_color_hexes" },
{ tag: "filament.multi_color_direction" },
{ tag: "filament.external_id" },
];
const filamentFields = useGetFields(EntityType.filament);
if (filamentFields.data !== undefined) {
filamentFields.data.forEach((field) => {
filamentTags.push({ tag: `filament.extra.${field.key}` });
});
}
const vendorTags = [
{ tag: "filament.vendor.id" },
{ tag: "filament.vendor.registered" },
{ tag: "filament.vendor.name" },
{ tag: "filament.vendor.comment" },
{ tag: "filament.vendor.empty_spool_weight" },
{ tag: "filament.vendor.external_id" },
];
const vendorFields = useGetFields(EntityType.vendor);
if (vendorFields.data !== undefined) {
vendorFields.data.forEach((field) => {
vendorTags.push({ tag: `filament.vendor.extra.${field.key}` });
});
}
const templateTags = [...spoolTags, ...filamentTags, ...vendorTags];
return (
<QRCodePrintingDialog
printSettings={selectedPrintSetting.labelSettings}
setPrintSettings={(newSettings) => {
selectedPrintSetting.labelSettings = newSettings;
updateCurrentPrintSettings(selectedPrintSetting);
}}
extraSettingsStart={
<>
<Form.Item label={t("printing.generic.settings")}>
<Flex gap={8}>
<Select
value={selectedSetting}
onChange={(value) => {
setSelectedSetting(value);
}}
options={
allPrintSettings &&
allPrintSettings.map((settings) => ({
label: settings.labelSettings.printSettings?.name || t("printing.generic.defaultSettings"),
value: settings.labelSettings.printSettings.id,
}))
}
></Select>
<Button
style={{ width: "3em" }}
icon={<PlusOutlined />}
title={t("printing.generic.addSettings")}
onClick={addNewPrintSettings}
/>
{allPrintSettings && allPrintSettings.length > 1 && (
<Popconfirm
title={t("printing.generic.deleteSettings")}
description={t("printing.generic.deleteSettingsConfirm")}
onConfirm={deleteCurrentPrintSettings}
okText={t("buttons.delete")}
cancelText={t("buttons.cancel")}
>
<Button
style={{ width: "3em" }}
danger
icon={<DeleteOutlined />}
title={t("printing.generic.deleteSettings")}
/>
</Popconfirm>
)}
</Flex>
</Form.Item>
<Form.Item label={t("printing.generic.settingsName")}>
<Input
value={selectedPrintSetting.labelSettings.printSettings?.name}
onChange={(e) => {
selectedPrintSetting.labelSettings.printSettings.name = e.target.value;
updateCurrentPrintSettings(selectedPrintSetting);
}}
/>
</Form.Item>
</>
}
items={items.map(function (spool) {
return {
value: `web+spoolman:s-${spool.id}`,
label: (
<p
style={{
padding: "1mm 1mm 1mm 0",
margin: 0,
whiteSpace: "pre-wrap",
}}
>
{renderLabelContents(template, spool)}
</p>
),
errorLevel: "H",
};
})}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.template")}>
<TextArea
value={template}
rows={8}
onChange={(newValue) => {
selectedPrintSetting.template = newValue.target.value;
updateCurrentPrintSettings(selectedPrintSetting);
}}
/>
</Form.Item>
<Modal open={templateHelpOpen} footer={null} onCancel={() => setTemplateHelpOpen(false)}>
<Table
size="small"
showHeader={false}
pagination={false}
scroll={{ y: 400 }}
columns={[{ dataIndex: "tag" }]}
dataSource={templateTags}
/>
</Modal>
<Text type="secondary">
{t("printing.qrcode.templateHelp")}{" "}
<Button size="small" onClick={() => setTemplateHelpOpen(true)}>
{t("actions.show")}
</Button>
</Text>
</>
}
/>
);
};
export default SpoolQRCodePrintingDialog;

View File

@@ -0,0 +1,213 @@
import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message, Button } from "antd";
import { ISpool } from "../spools/model";
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "../../components/column";
import { TableState } from "../../utils/saveload";
import { useTable } from "@refinedev/antd";
import { t } from "i18next";
import { useSpoolmanFilamentFilter, useSpoolmanMaterials } from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";
import { useNavigate } from "react-router-dom";
interface Props {
description?: string;
onContinue: (selectedSpools: ISpool[]) => void;
}
interface ISpoolCollapsed extends ISpool {
"filament.combined_name": string;
"filament.id": number;
"filament.material"?: string;
}
function collapseSpool(element: ISpool): ISpoolCollapsed {
let filament_name: string;
if (element.filament.vendor && "name" in element.filament.vendor) {
filament_name = `${element.filament.vendor.name} - ${element.filament.name}`;
} else {
filament_name = element.filament.name ?? element.filament.id.toString();
}
return {
...element,
"filament.combined_name": filament_name,
"filament.id": element.filament.id,
"filament.material": element.filament.material,
};
}
const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [showArchived, setShowArchived] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const navigate = useNavigate();
const { tableProps, sorters, filters, current, pageSize } = useTable<ISpoolCollapsed>({
resource: "spool",
meta: {
queryParams: {
["allow_archived"]: showArchived,
},
},
syncWithLocation: false,
pagination: {
mode: "off",
current: 1,
pageSize: 10,
},
sorters: {
mode: "server",
},
filters: {
mode: "server",
},
queryOptions: {
select(data) {
return {
total: data.total,
data: data.data.map(collapseSpool),
};
},
},
});
// Store state in local storage
const tableState: TableState = {
sorters,
filters,
pagination: { current, pageSize },
};
// Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: ISpoolCollapsed[] = React.useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
);
// Function to add/remove all filtered items from selected items
const selectUnselectFiltered = (select: boolean) => {
setSelectedItems((prevSelected) => {
const filtered = dataSource.map((spool) => spool.id).filter((spool) => !prevSelected.includes(spool));
return select ? [...prevSelected, ...filtered] : filtered;
});
};
// Handler for selecting/unselecting individual items
const handleSelectItem = (item: number) => {
setSelectedItems((prevSelected) =>
prevSelected.includes(item) ? prevSelected.filter((selected) => selected !== item) : [...prevSelected, item]
);
};
// State for the select/unselect all checkbox
const isAllFilteredSelected = dataSource.every((spool) => selectedItems.includes(spool.id));
const isSomeButNotAllFilteredSelected =
dataSource.some((spool) => selectedItems.includes(spool.id)) && !isAllFilteredSelected;
const commonProps = {
t,
navigate,
actions: () => {
return [];
},
dataSource,
tableState,
sorter: true,
};
return (
<>
{contextHolder}
<Space direction="vertical" style={{ width: "100%" }}>
{description && <div>{description}</div>}
<Table
{...tableProps}
rowKey="id"
tableLayout="auto"
dataSource={dataSource}
pagination={false}
scroll={{ y: 200 }}
columns={removeUndefined([
{
width: 50,
render: (_, item: ISpool) => (
<Checkbox checked={selectedItems.includes(item.id)} onChange={() => handleSelectItem(item.id)} />
),
},
SortedColumn({
...commonProps,
id: "id",
i18ncat: "spool",
width: 80,
}),
SpoolIconColumn({
...commonProps,
id: "filament.combined_name",
dataId: "filament.combined_name",
i18nkey: "spool.fields.filament_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
filterValueQuery: useSpoolmanFilamentFilter(),
}),
FilteredQueryColumn({
...commonProps,
id: "filament.material",
i18nkey: "spool.fields.material",
filterValueQuery: useSpoolmanMaterials(),
}),
])}
/>
<Row>
<Col span={12}>
<Checkbox
checked={isAllFilteredSelected}
indeterminate={isSomeButNotAllFilteredSelected}
onChange={(e) => {
selectUnselectFiltered(e.target.checked);
}}
>
{t("printing.spoolSelect.selectAll")}
</Checkbox>
</Col>
<Col span={12}>
<div style={{ float: "right" }}>
{t("printing.spoolSelect.selectedTotal", {
count: selectedItems.length,
})}
</div>
</Col>
<Col span={12}>
<Checkbox
checked={showArchived}
onChange={(e) => {
setShowArchived(e.target.checked);
if (!e.target.checked) {
// Remove archived spools from selected items
setSelectedItems((prevSelected) =>
prevSelected.filter(
(selected) => dataSource.find((spool) => spool.id === selected)?.archived !== true
)
);
}
}}
>
{t("printing.spoolSelect.showArchived")}
</Checkbox>
</Col>
</Row>
</Space>
<Button
onClick={() => {
if (selectedItems.length === 0) {
messageApi.open({
type: "error",
content: t("printing.spoolSelect.noSpoolsSelected"),
});
return;
}
onContinue(dataSource.filter((spool) => selectedItems.includes(spool.id)));
}}
></Button>
</>
);
};
export default SpoolSelectModal;