From 93992a0d3a51422eca7d0f8dff08b3d982469ca9 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 12 Jun 2026 14:52:54 +0700 Subject: [PATCH] Add thermal printer integration with USB and Bluetooth support. Implement receipt printing functionality using @point-of-sale libraries. Create UI components for printer connection management and order receipt printing. Update OrderStatus and app settings to accommodate new features. --- app/Enums/OrderStatus.php | 2 +- app/Http/Middleware/HandleInertiaRequests.php | 4 +- package-lock.json | 59 +- package.json | 3 + resources/js/app.ts | 2 + .../admin/manage/orders/OrderGroupedTable.vue | 3 - .../admin/manage/orders/OrderPrintButton.vue | 91 +++ .../orders/ThermalPrinterConnectButton.vue | 130 ++++ .../manage/orders/data-table-actions.vue | 3 + resources/js/composables/useThermalPrinter.ts | 104 ++++ .../thermal-printer/encode-order-receipt.ts | 134 +++++ .../normalize-encoder-options.ts | 64 ++ .../lib/thermal-printer/stable-transport.ts | 565 ++++++++++++++++++ resources/js/lib/thermal-printer/types.ts | 16 + .../js/pages/admin/manage/orders/Index.vue | 17 +- resources/js/types/global.d.ts | 1 + resources/js/types/point-of-sale.d.ts | 61 ++ resources/js/types/thermal-bluetooth.d.ts | 57 ++ 18 files changed, 1304 insertions(+), 12 deletions(-) create mode 100644 resources/js/components/admin/manage/orders/OrderPrintButton.vue create mode 100644 resources/js/components/admin/manage/orders/ThermalPrinterConnectButton.vue create mode 100644 resources/js/composables/useThermalPrinter.ts create mode 100644 resources/js/lib/thermal-printer/encode-order-receipt.ts create mode 100644 resources/js/lib/thermal-printer/normalize-encoder-options.ts create mode 100644 resources/js/lib/thermal-printer/stable-transport.ts create mode 100644 resources/js/lib/thermal-printer/types.ts create mode 100644 resources/js/types/point-of-sale.d.ts create mode 100644 resources/js/types/thermal-bluetooth.d.ts diff --git a/app/Enums/OrderStatus.php b/app/Enums/OrderStatus.php index 570ed4e..4b19b82 100644 --- a/app/Enums/OrderStatus.php +++ b/app/Enums/OrderStatus.php @@ -76,7 +76,7 @@ public function availableActions(): array 'label' => 'Selesai', 'destructive' => false, 'permission' => Permission::ORDERS_COMPLETE->value, - 'icon_only' => false, + 'icon_only' => true, ], [ 'status' => self::CANCELLED->value, diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index b6674f7..c17e7f6 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -2,6 +2,7 @@ namespace App\Http\Middleware; +use App\Settings\SystemSettings; use Illuminate\Http\Request; use Inertia\Middleware; @@ -37,7 +38,8 @@ public function share(Request $request): array { return [ ...parent::share($request), - 'name' => config('app.name'), + 'name' => fn () => app(SystemSettings::class)->app_name, + 'address' => fn () => app(SystemSettings::class)->address, 'auth' => fn () => [ 'user' => $request->user() ? [ 'id' => $request->user()->id, diff --git a/package-lock.json b/package-lock.json index 1a94fe3..60ce9b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,9 @@ "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", "@lucide/vue": "^1.17.0", + "@point-of-sale/receipt-printer-encoder": "^3.0.3", + "@point-of-sale/webbluetooth-receipt-printer": "^2.0.0", + "@point-of-sale/webserial-receipt-printer": "^2.0.0", "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", "@vitejs/plugin-vue": "^6.0.0", @@ -1681,7 +1684,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz", "integrity": "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==", - "dev": true, "license": "MIT" }, "node_modules/@emnapi/core": { @@ -2689,6 +2691,37 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@point-of-sale/codepage-encoder": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@point-of-sale/codepage-encoder/-/codepage-encoder-3.0.2.tgz", + "integrity": "sha512-Sx+0sE/XgPtMUNKScHpwTIZiHNd/+Mg0udACQJVwPsxH9kUAgIkRi9kRlmWBMCruwOJadjKLyKs8MO8Bh+VS/A==", + "license": "MIT" + }, + "node_modules/@point-of-sale/receipt-printer-encoder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@point-of-sale/receipt-printer-encoder/-/receipt-printer-encoder-3.0.3.tgz", + "integrity": "sha512-2+xgs6rwNfrkEw9g4LkgQS6k29qnmo3+hOV7Z2xHNBFn8h4FrwquZwL/dBOTwgmQ9sWEYBkj/bQkg9Rz6lL99w==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "@point-of-sale/codepage-encoder": "^3.0.0", + "canvas-dither": "^1.0.1", + "canvas-flatten": "^1.0.1", + "resize-image-data": "^0.3.1" + } + }, + "node_modules/@point-of-sale/webbluetooth-receipt-printer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@point-of-sale/webbluetooth-receipt-printer/-/webbluetooth-receipt-printer-2.0.0.tgz", + "integrity": "sha512-xtplxsZWuhKa7DFASYzV8PSiRzwzjbQLqAsWSdjpIVkEo9Xmo652Pcu3aKbRUu4Ih2k9Fz4weyUZljOgbyDYkg==", + "license": "MIT" + }, + "node_modules/@point-of-sale/webserial-receipt-printer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@point-of-sale/webserial-receipt-printer/-/webserial-receipt-printer-2.0.0.tgz", + "integrity": "sha512-txTb+DTdkrl2c3oxtIAYc7vKGb4fJMPGnC8iCYMETeAhcFZP2yeISyCa0DVgw4YXNCmsOv6mI6aYzqUjuBb+wQ==", + "license": "MIT" + }, "node_modules/@quansync/fs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", @@ -5236,6 +5269,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-dither": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/canvas-dither/-/canvas-dither-1.0.1.tgz", + "integrity": "sha512-wT1RnV2y9SauibcsfMMCagQTnXmyQq2nJXuX5tTEvo0sXm1vVbJ2r8QxSC3fRQMY3ZgZVq1j54cOvmO4u+Lu/A==", + "license": "MIT" + }, + "node_modules/canvas-flatten": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/canvas-flatten/-/canvas-flatten-1.0.1.tgz", + "integrity": "sha512-UhHeKeWGS23Bm5teZiHk6NEOme1OKCpv6arFovV89dydt0Xz+U4HkPXAKBZS/hmpR15x5U9tDj1ieMQMTwvCAA==", + "license": "MIT" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8965,6 +9010,18 @@ "node": ">=0.10.0" } }, + "node_modules/resize-image-data": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/resize-image-data/-/resize-image-data-0.3.1.tgz", + "integrity": "sha512-6hVRn2S6W1cdycreA6Vth5XRN2NnGs7/RnVpxNw/1OCK8aCoevRFH2WprmQRZDnnH3e6awLv2tTIPuv7/7xeGg==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", diff --git a/package.json b/package.json index 45aadc4..177df18 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,9 @@ "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", "@lucide/vue": "^1.17.0", + "@point-of-sale/receipt-printer-encoder": "^3.0.3", + "@point-of-sale/webbluetooth-receipt-printer": "^2.0.0", + "@point-of-sale/webserial-receipt-printer": "^2.0.0", "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", "@vitejs/plugin-vue": "^6.0.0", diff --git a/resources/js/app.ts b/resources/js/app.ts index b971d56..47abc97 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -6,8 +6,10 @@ import { createApp, h } from 'vue'; import 'vue-sonner/style.css'; import { Toaster } from '@/components/ui/sonner'; import { useFlashToast } from '@/composables/useFlashToast'; +import { restoreConnection } from '@/lib/thermal-printer/stable-transport'; registerSW({ immediate: true }); +void restoreConnection(); const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; diff --git a/resources/js/components/admin/manage/orders/OrderGroupedTable.vue b/resources/js/components/admin/manage/orders/OrderGroupedTable.vue index 3cf630b..72b0485 100644 --- a/resources/js/components/admin/manage/orders/OrderGroupedTable.vue +++ b/resources/js/components/admin/manage/orders/OrderGroupedTable.vue @@ -103,9 +103,6 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'

{{ order.customer.name }}

-

- Tanpa pelanggan -

{{ order.created_at_formatted }}

Oleh {{ order.created_by?.profile?.full_name ?? order.created_by?.username }}

diff --git a/resources/js/components/admin/manage/orders/OrderPrintButton.vue b/resources/js/components/admin/manage/orders/OrderPrintButton.vue new file mode 100644 index 0000000..64b673d --- /dev/null +++ b/resources/js/components/admin/manage/orders/OrderPrintButton.vue @@ -0,0 +1,91 @@ + + + diff --git a/resources/js/components/admin/manage/orders/ThermalPrinterConnectButton.vue b/resources/js/components/admin/manage/orders/ThermalPrinterConnectButton.vue new file mode 100644 index 0000000..a578f2c --- /dev/null +++ b/resources/js/components/admin/manage/orders/ThermalPrinterConnectButton.vue @@ -0,0 +1,130 @@ + + + diff --git a/resources/js/components/admin/manage/orders/data-table-actions.vue b/resources/js/components/admin/manage/orders/data-table-actions.vue index 7446f31..0f6e7a1 100644 --- a/resources/js/components/admin/manage/orders/data-table-actions.vue +++ b/resources/js/components/admin/manage/orders/data-table-actions.vue @@ -3,6 +3,7 @@ import { Link, router } from '@inertiajs/vue3'; import { Check, Pencil, Send, Trash2, X } from '@lucide/vue'; import { computed, ref } from 'vue'; import { toast } from 'vue-sonner'; +import OrderPrintButton from '@/components/admin/manage/orders/OrderPrintButton.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -105,6 +106,8 @@ function actionIcon(status: string) {