Skip to content

Instantly share code, notes, and snippets.

@solanoize
Created May 21, 2024 11:27
Show Gist options
  • Select an option

  • Save solanoize/5ee436ceeb7f6a30431938fadb69b82a to your computer and use it in GitHub Desktop.

Select an option

Save solanoize/5ee436ceeb7f6a30431938fadb69b82a to your computer and use it in GitHub Desktop.
Thermal Printer React
import axios from "axios";
import { useCallback, useEffect, useMemo, useState } from "react"
import { Badge, Button, Card, Col, Container, Form, Row, Table } from "react-bootstrap";
import useFormatter from "../../hooks/useFormatter";
import { FaBluetooth, FaCartPlus, FaPrint, FaTrash } from "react-icons/fa";
import useThermal, { StandardInvoice, StandardItemInvoice } from "../../hooks/useThermal";
const PageProduct = () => {
const formatter = useFormatter();
const thermal = useThermal();
const [products, setProducts] = useState([]);
const [items, setItems] = useState([]);
const [order, setOrder] = useState({
total: 0,
dibayar: 0,
kembali: 0,
items: []
})
const [orderValidator, setOrderValidator] = useState({
total: "",
dibayar: "",
kembali: ""
})
const onGetProducts = () => {
axios.get(`${import.meta.env.VITE_BASE_URL}/products`)
.then((response) => {
setProducts(response.data);
}).catch((error) => {
alert(`Error ${error}`);
});
}
const onAddItem = (value) => {
let item = items.find((val) => val.id === value.id);
if (item) {
item.qty += 1;
item.subtotal = item.qty * item.price;
let index = items.findIndex((val) => val.id === value.id);
setItems((values) => {
let itemsCopy = [...values]
itemsCopy.splice(index, 1, item);
return itemsCopy;
});
} else {
value.qty = 1;
value.subtotal = value.qty * Number(value.price);
setItems([...items, value])
}
}
const removeItem = (index) => {
setItems((values) => {
let itemsCopy = [...values];
itemsCopy.splice(index, 1);
return itemsCopy;
})
}
const getTotal = useMemo(() => {
const subtotals = items.map((value) => value.subtotal);
let count = 0;
for (let subtotal of subtotals) {
count += subtotal;
}
return count;
}, [items]);
const onHandlerOrder = (e) => {
const name = e.target.name;
const type = e.target.type;
let value = e.target.value;
if (type === 'number') {
value = Number(value.replace(/\D/g, ''))
}
setOrderValidator((values) => ({...values, [name]: e.target.validationMessage}))
setOrder((values) => ({...values, [name]: value}))
}
const onCalculateTotal = useCallback(() => {
/**
* useCallback adaalah memoisasi sebagai penyimpan nilai dalam cache
* sehingga tidak perlu dihitung ulang.
*
* useMemo hampir mirip, hanya saja fungsionalitasnya berbeda.
* useMemo mengembalikan value (cocok untuk perhitungan yang butuh data external)
* sedangkan useCallback mengembalikan fungsi agar tidak di kalkulate (render) ulang
* jika dependensinya tidak berubah, cocok untuk ditempatkan pada useEffect
* dengan dependensi nama fungsinya sendiri
*/
const subtotals = items.map((value) => value.subtotal);
let count = 0;
for (let subtotal of subtotals) {
count += subtotal;
}
setOrder((values) => ({...values, total: count, dibayar: 0}));
}, [items]);
useEffect(() => {
console.log("useEffect 1")
onGetProducts()
}, []);
useEffect(() => {
console.log("useEffect 2")
onCalculateTotal()
}, [onCalculateTotal])
useEffect(() => {
console.log("useEffect 3")
if (order.dibayar >= order.total) {
setOrder((values) => ({
...values,
kembali: Math.abs(order.total - order.dibayar)
}));
} else {
setOrder((values) => ({
...values,
kembali: 0
}));
}
}, [order.dibayar, order.total])
const printInvoice = () => {
let stdItems = items.map((value) => {
return new StandardItemInvoice(
value.title,
value.qty,
formatter.toCurrency(value.price),
formatter.toCurrency(value.subtotal)
)
})
let standardInvoice = new StandardInvoice(
"TRX001",
new Date().toISOString(),
formatter.toCurrency(order.total),
formatter.toCurrency(order.kembali),
formatter.toCurrency(order.dibayar),
stdItems
);
thermal.printInvoice(standardInvoice);
}
return (
<>
<Container className="mt-4">
<Row className="mb-4">
<Col className="d-flex justify-content-between">
<h4>Simple POS</h4>
{thermal.isConnect ? (
<Button onClick={thermal.disconnect} variant="primary">
<FaBluetooth /> Printer Connected
</Button>
) : (
<Button onClick={thermal.connect} variant="success">
<FaBluetooth /> Connect Thermal
</Button>
)}
</Col>
</Row>
<Row>
<Col>
<Row>
{products.map((value) => (
<Col md={4} key={value.id} className="mb-4">
<Card >
<Card.Img variant="top" src={`${value.image},pizza?random${value.id}`} />
<Card.Body>
<Card.Title>{value.title}</Card.Title>
<Card.Text>
<small className="text-muted">{value.description}</small>
</Card.Text>
</Card.Body>
<Card.Footer className="d-flex justify-content-between align-items-end">
<Badge>{formatter.toCurrency(value.price)}</Badge>
<Button onClick={() => onAddItem(value)} size="sm">
<FaCartPlus /> Add
</Button>
</Card.Footer>
</Card>
</Col>
))}
</Row>
</Col>
<Col md={4}>
<Card>
<Card.Body>
<Card.Title>Items</Card.Title>
</Card.Body>
<Table hover striped responsive>
<thead>
<tr>
<th>Title</th>
<th>Price</th>
<th>Qty</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
{items.map((value, index) => (
<tr key={value.id}>
<td >{value.title}</td>
<td>{formatter.toCurrency(value.price)}</td>
<td>{value.qty}</td>
<td>{formatter.toCurrency(value.subtotal)}</td>
<td>
<Button onClick={() => removeItem(index)} variant="danger" size="sm">
<FaTrash />
</Button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={3}>Total</td>
<th colSpan={2}>
{formatter.toCurrency(getTotal)}
</th>
</tr>
<tr>
<td colSpan={3}>Dibayar</td>
<td colSpan={2}>
<Form.Group>
<Form.Control
name="dibayar"
type="number"
value={order.dibayar ? order.dibayar.toString() : ""}
min={getTotal}
required
onChange={onHandlerOrder}
/>
{orderValidator.dibayar && (
<small className="text-danger">{orderValidator.dibayar}</small>
)}
</Form.Group>
</td>
</tr>
<tr>
<td colSpan={3}>Kembali</td>
<th colSpan={2}>
{formatter.toCurrency(order.kembali)}
</th>
</tr>
<tr>
<td colSpan={5}>
<Button onClick={printInvoice}>
<FaPrint />
</Button>
</td>
</tr>
</tfoot>
</Table>
</Card>
</Col>
</Row>
</Container>
</>
)
}
export default PageProduct;
import { useState } from "react"
export class StandardItemInvoice {
constructor(title, qty, price, subtotal) {
this.title = title;
this.qty = qty;
this.price = price;
this.subtotal = subtotal;
}
}
export class StandardInvoice {
constructor(invoiceNumber, invoiceDate, invoiceTotal, invoiceReturn, invoicePayout, standardItemInvoice) {
this.invoiceNumber = invoiceNumber;
this.invoiceDate = invoiceDate;
this.invoiceTotal = invoiceTotal;
this.invoiceReturn = invoiceReturn;
this.invoicePayout = invoicePayout;
this.items = standardItemInvoice;
}
}
const useThermal = () => {
const SERVICE_UUID = "000018f0-0000-1000-8000-00805f9b34fb"
const SERVICE_CHARACTERISTIC = "00002af1-0000-1000-8000-00805f9b34fb"
const [isConnect, setIsConnect] = useState(false);
const [characteristic, setCharacteristic] = useState(null);
const [printer, setPrinter] = useState(null);
const escape = '\x1B';
const newLine = `\x0A`;
const alignCenter = (value) => '\x1b' + value.toString() + '\x01';
const alignLeft = (value) => '\x1b' + value.toString() + '\x00';
const alignRight = (value) => '\x1b' + value.toString() + '\x02';
const reset = () => '\x1B' + '\x61' + '\x31' + '\x1D' + '\x21' + '\x00' + '\n'.repeat(2) + '\r';
const enter = (count) => '\n'.repeat(count) + '\r';
const fontNormal = (value) => '\x1D' + '\x21' + '\x00' + value.toString();
const fontLarge = (value) => '\x1D' + '\x21' + '\x11' + value.toString();
const centerLine = (count) => '\x1B' + '\x61' + '\x31' + '-'.repeat(count);
const printInvoice = async (standardObjectInvoice) => {
// const text = [];
// text.push(escape + "@");
// text.push(alignLeft(fontLarge(import.meta.env.VITE_APP_NAME)))
// text.push(enter(1));
// text.push(centerLine(10));
// text.push(enter(2));
// text.push(alignLeft(fontNormal(" ")));
// for (let item of standardObjectInvoice.items) {
// text.push(alignLeft(fontNormal(item.title)));
// text.push(enter(1));
// text.push(alignLeft(fontNormal(`${item.qty} x ${item.price} @ ${item.subtotal}`)));
// text.push(enter(2));
// }
// text.push(alignCenter(fontNormal(`Thank You`)));
// text.push(alignLeft(""));
// text.push(enter(3));
// text.push(reset());
var esc = '\x1B'; //ESC byte in hex notation
var newLine = '\x0A'; //LF byte in hex notation
var cmds = esc + "@"; //Initializes the printer (ESC @)
cmds += esc + '!' + '\x38'; //Emphasized + Double-height + Double-width mode selected (ESC ! (8 + 16 + 32)) 56 dec => 38 hex
cmds += import.meta.env.VITE_APP_NAME.toUpperCase(); //text to print
cmds += newLine + newLine;
cmds += esc + '!' + '\x00'; //Character font A selected (ESC ! 0)
for (let item of standardObjectInvoice.items) {
cmds += `${item.title} ${item.subtotal}'`
cmds += newLine;
}
cmds += newLine + newLine;
cmds += `TOTAL ${standardObjectInvoice.invoiceTotal}`;
cmds += newLine;
cmds += `DIBAYAR ${standardObjectInvoice.invoicePayout}`;
cmds += newLine;
cmds += `DIBAYAR ${standardObjectInvoice.invoiceReturn}`;
// cmds += newLine;
// cmds += 'TOTAL 9.22';
// cmds += newLine;
// cmds += 'CASH TEND 10.00';
// cmds += newLine;
// cmds += 'CASH DUE 0.78';
cmds += newLine + newLine;
cmds += esc + '!' + '\x18'; //Emphasized + Double-height mode selected (ESC ! (16 + 8)) 24 dec => 18 hex
cmds += '# ITEMS SOLD ' + standardObjectInvoice.items.length.toString();
cmds += esc + '!' + '\x00'; //Character font A selected (ESC ! 0)
cmds += newLine + newLine;
cmds += standardObjectInvoice.invoiceDate;
cmds += newLine + newLine;
printing(cmds)
}
const printing = async (text) => {
let textEncoder = new TextEncoder("utf-8");
text = textEncoder.encode(text);
await characteristic.writeValue(text);
}
const disconnect = () => {
printer.gatt.disconnect();
setPrinter(null);
setCharacteristic(null);
setIsConnect(false)
}
const connect = () => {
navigator.bluetooth.requestDevice({
filters: [{
services: [SERVICE_UUID]
}]
}).then((device) => {
setPrinter(device);
return device.gatt.connect();
})
.then((server) => server.getPrimaryService(SERVICE_UUID))
.then((service) => service.getCharacteristic(SERVICE_CHARACTERISTIC))
.then((chrt) => {
setIsConnect(true);
setCharacteristic(chrt)
})
.catch(error => {
setIsConnect(false);
setCharacteristic(null)
console.error(error);
});
}
return {connect, disconnect, isConnect, printInvoice}
}
export default useThermal;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment