diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 0473a06..bedabed 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; @@ -38,6 +39,7 @@ public function share(Request $request): array return [ ...parent::share($request), 'name' => config('app.name'), + 'address' => app(SystemSettings::class)->address ?? '', 'auth' => [ 'user' => $request->user() ? tap($request->user()->load('userProfile', 'roles'), function ($user) { diff --git a/package-lock.json b/package-lock.json index 89ea01d..72b53bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@inertiajs/react": "^3.0.0", "@inertiajs/vite": "^3.0.0", "@laravel/passkeys": "^0.2.0", + "@point-of-sale/receipt-printer-encoder": "^3.0.3", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", @@ -1779,6 +1780,12 @@ } } }, + "node_modules/@canvas/image-data": { + "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==", + "license": "MIT" + }, "node_modules/@date-fns/tz": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", @@ -2043,37 +2050,6 @@ "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==", "license": "BSD-3-Clause" }, - "node_modules/@emnapi/core": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", - "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "2.0.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", - "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", - "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -2583,27 +2559,6 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", - "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^2.0.0-alpha.3", - "@emnapi/runtime": "^2.0.0-alpha.3" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2648,6 +2603,25 @@ "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/@radix-ui/number": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", @@ -4170,150 +4144,6 @@ } } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", @@ -4346,103 +4176,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -5041,118 +4774,6 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", @@ -5185,51 +4806,6 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", @@ -5356,16 +4932,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5854,216 +5420,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", @@ -6092,115 +5448,6 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -6860,6 +6107,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", @@ -12313,6 +11572,18 @@ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, + "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 f18e10e..e7d2904 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@inertiajs/react": "^3.0.0", "@inertiajs/vite": "^3.0.0", "@laravel/passkeys": "^0.2.0", + "@point-of-sale/receipt-printer-encoder": "^3.0.3", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", diff --git a/resources/js/hooks/use-thermal-printer.ts b/resources/js/hooks/use-thermal-printer.ts new file mode 100644 index 0000000..8e1e84c --- /dev/null +++ b/resources/js/hooks/use-thermal-printer.ts @@ -0,0 +1,368 @@ +import ReceiptPrinterEncoder from '@point-of-sale/receipt-printer-encoder'; +import { useCallback, useRef, useState } from 'react'; + +export type PrinterConnectionType = 'bluetooth' | 'usb'; +export type PaperWidth = 58 | 80; + +interface PrinterState { + connected: boolean; + connectionType: PrinterConnectionType | null; + printerName: string | null; +} + +/* ── Receipt Builder (persis seperti contoh Vue) ── */ + +export function getPaperColumns(paperWidth: PaperWidth): number { + return paperWidth === 58 ? 32 : 48; +} + +function dashedLine(columns: number): string { + return '-'.repeat(columns); +} + +export type OrderReceiptItem = { + product_name: string; + variant_name: string; + quantity: string; + unit_price: string; + subtotal: string; +}; + +export type OrderReceiptData = { + order_number: string; + created_at: string; + customer_name: string | null; + cashier_name: string; + items: OrderReceiptItem[]; + subtotal: string; + discount: string; + nego_price: string | null; + total_amount: string; + notes: string | null; +}; + +export type EncodeOrderReceiptOptions = { + storeName: string; + storeAddress: string; + paperWidth: PaperWidth; +}; + +export function encodeOrderReceipt( + order: OrderReceiptData, + options: EncodeOrderReceiptOptions, +): Uint8Array { + const columns = getPaperColumns(options.paperWidth); + const amountColumnWidth = 14; + const labelColumnWidth = columns - amountColumnWidth; + + const encoder = new ReceiptPrinterEncoder({ columns }); + + encoder + .initialize() + .align('center') + .bold(true) + .text('') + .newline() + .text(options.storeName) + .newline() + .bold(false) + .newline(); + + for (const addressLine of options.storeAddress.split('\n')) { + const trimmed = addressLine.trim(); + if (trimmed) { + encoder.align('center').text(trimmed).newline(); + } + } + + encoder.align('center').line(dashedLine(columns)); + + encoder + .align('left') + .line(`No : ${order.order_number}`) + .line(`Tgl : ${order.created_at}`); + + if (order.customer_name) { + encoder.line(`Cust : ${order.customer_name}`); + } + + encoder + .line(`Kasir: ${order.cashier_name}`) + .line(dashedLine(columns)); + + for (const item of order.items) { + encoder + .line(item.product_name) + .line(` ${item.variant_name}`) + .table( + [ + { width: labelColumnWidth, align: 'left' }, + { width: amountColumnWidth, align: 'right' }, + ], + [ + [ + `${item.quantity} x ${item.unit_price}`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (rowEncoder: any) => + rowEncoder + .bold() + .text(item.subtotal) + .bold(false), + ], + ], + ) + .newline(); + } + + encoder.line(dashedLine(columns)); + + const summaryRows = [ + ['Subtotal', order.subtotal], + ['Diskon', order.discount], + ]; + + if (order.nego_price) { + summaryRows.push(['Harga Nego', order.nego_price]); + } + + summaryRows.push(['Total', order.total_amount]); + + encoder.table( + [ + { width: labelColumnWidth, align: 'left' }, + { width: amountColumnWidth, align: 'right' }, + ], + summaryRows, + ); + + encoder + .newline() + .line(dashedLine(columns)) + .align('center') + .line('Terima kasih atas kunjungannya') + .line('Silakan berbelanja kembali') + .newline(3) + .cut(); + + return encoder.encode(); +} + +/* ── Printer Connection Hook ── */ + +export function useThermalPrinter() { + const [state, setState] = useState({ + connected: false, + connectionType: null, + printerName: null, + }); + + const bluetoothDeviceRef = useRef(null); + const bluetoothCharacteristicRef = + useRef(null); + const usbDeviceRef = useRef(null); + + const disconnect = useCallback(async () => { + try { + if ( + state.connectionType === 'bluetooth' && + bluetoothDeviceRef.current + ) { + bluetoothDeviceRef.current.gatt?.disconnect(); + bluetoothDeviceRef.current = null; + bluetoothCharacteristicRef.current = null; + } + + if (state.connectionType === 'usb' && usbDeviceRef.current) { + usbDeviceRef.current.close(); + usbDeviceRef.current = null; + } + } catch { + // silent + } + + setState({ + connected: false, + connectionType: null, + printerName: null, + }); + }, [state.connectionType]); + + const connectBluetooth = useCallback(async () => { + if (!navigator.bluetooth) { + throw new Error( + 'Web Bluetooth tidak didukung di browser ini. Gunakan Chrome atau Edge.', + ); + } + + const device = await navigator.bluetooth.requestDevice({ + acceptAllDevices: true, + optionalServices: [ + '000018f0-0000-1000-8000-00805f9b34fb', + '00001800-0000-1000-8000-00805f9b34fb', + '00001801-0000-1000-8000-00805f9b34fb', + '00001101-0000-1000-8000-00805f9b34fb', + '0000ffe0-0000-1000-8000-00805f9b34fb', + '0000fee7-0000-1000-8000-00805f9b34fb', + ], + }); + + const server = await device.gatt?.connect(); + + if (!server) { + throw new Error('Gagal terhubung ke perangkat Bluetooth.'); + } + + let characteristic: BluetoothCharacteristic | null = null; + + const services = await server.getPrimaryServices(); + for (const svc of services) { + try { + const chars = await svc.getCharacteristics(); + const writable = chars.find( + (c) => + c.properties.writeWithoutResponse || + c.properties.write, + ); + if (writable) { + characteristic = writable; + break; + } + } catch { + // skip + } + } + + if (!characteristic) { + throw new Error( + 'Tidak ditemukan characteristic yang bisa ditulis di printer ini.', + ); + } + + bluetoothDeviceRef.current = device; + bluetoothCharacteristicRef.current = characteristic; + + setState({ + connected: true, + connectionType: 'bluetooth', + printerName: device.name ?? 'Printer Bluetooth', + }); + + return device.name ?? 'Printer Bluetooth'; + }, []); + + const connectUSB = useCallback(async () => { + if (!navigator.usb) { + throw new Error( + 'Web USB tidak didukung di browser ini. Gunakan Chrome atau Edge.', + ); + } + + const device = await navigator.usb.requestDevice({ filters: [] }); + await device.open(); + + if (device.configuration === null) { + await device.selectConfiguration(1); + } + + const iface = + device.configuration?.interfaces.find((i) => + i.alternate.endpoints.some((e) => e.direction === 'out'), + ) ?? device.configuration?.interfaces[0]; + + if (iface) { + await device.claimInterface(iface.interfaceNumber); + } + + usbDeviceRef.current = device; + + setState({ + connected: true, + connectionType: 'usb', + printerName: device.productName ?? 'Printer USB', + }); + + return device.productName ?? 'Printer USB'; + }, []); + + const sendToBluetooth = useCallback(async (data: Uint8Array) => { + const characteristic = bluetoothCharacteristicRef.current; + if (!characteristic) { + throw new Error( + 'Printer Bluetooth tidak terhubung.', + ); + } + + const useWriteWithoutResponse = + characteristic.properties.writeWithoutResponse; + const chunkSize = 128; + + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = data.slice(i, i + chunkSize); + if (useWriteWithoutResponse) { + await characteristic.writeValueWithoutResponse(chunk); + } else { + await characteristic.writeValueWithResponse(chunk); + } + await new Promise((r) => setTimeout(r, 10)); + } + }, []); + + const sendToUSB = useCallback(async (data: Uint8Array) => { + const device = usbDeviceRef.current; + if (!device) { + throw new Error('Printer USB tidak terhubung.'); + } + + const iface = device.configuration?.interfaces.find((i) => + i.alternate.endpoints.some((e) => e.direction === 'out'), + ); + const endpoint = + iface?.alternate.endpoints.find( + (e) => e.direction === 'out', + )?.endpointNumber ?? 1; + + const chunkSize = 512; + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = data.slice(i, i + chunkSize); + await device.transferOut(endpoint, chunk); + } + }, []); + + const print = useCallback( + async (data: Uint8Array) => { + if (!state.connected) { + throw new Error('Tidak ada printer yang terhubung.'); + } + + if (state.connectionType === 'bluetooth') { + await sendToBluetooth(data); + } else if (state.connectionType === 'usb') { + await sendToUSB(data); + } + }, + [state.connected, state.connectionType, sendToBluetooth, sendToUSB], + ); + + const connect = useCallback( + async (type: PrinterConnectionType) => { + if (state.connected) { + await disconnect(); + } + + if (type === 'bluetooth') { + return await connectBluetooth(); + } + return await connectUSB(); + }, + [state.connected, disconnect, connectBluetooth, connectUSB], + ); + + return { + connected: state.connected, + connectionType: state.connectionType, + printerName: state.printerName, + connect, + disconnect, + print, + }; +} diff --git a/resources/js/pages/admin/manage/transaction/index.tsx b/resources/js/pages/admin/manage/transaction/index.tsx index 42ae517..ba67100 100644 --- a/resources/js/pages/admin/manage/transaction/index.tsx +++ b/resources/js/pages/admin/manage/transaction/index.tsx @@ -1,7 +1,8 @@ -import { Head, Link, router } from '@inertiajs/react'; +import { Head, Link, router, usePage } from '@inertiajs/react'; import { format } from 'date-fns'; -import { Plus } from 'lucide-react'; +import { Bluetooth, Cable, Plus, Printer, Unplug } from 'lucide-react'; import { useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { CardTable } from '@/components/card-table'; import { DatePicker } from '@/components/date-picker'; import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; @@ -17,6 +18,13 @@ import { ComboboxItem, ComboboxList, } from '@/components/ui/combobox'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { Select, SelectContent, @@ -26,6 +34,7 @@ import { } from '@/components/ui/select'; import { useCan } from '@/hooks/use-can'; import { useServerTable } from '@/hooks/use-server-table'; +import { useThermalPrinter, encodeOrderReceipt } from '@/hooks/use-thermal-printer'; import { destroy, create as transactionCreate, @@ -89,8 +98,10 @@ export default function TransactionIndex({ filterOptions, }: Props) { const { can } = useCan(); + const { name: appName, address: appAddress } = usePage().props as unknown as { name: string; address: string }; const [deleting, setDeleting] = useState(null); const expand = useCardTableExpand(true); + const printer = useThermalPrinter(); const pagination = { current_page: transactions.current_page, @@ -152,6 +163,52 @@ export default function TransactionIndex({ router.patch(transactionUpdateStatus.url(transaction.id), { status }); } + function handlePrintReceipt(transaction: Transaction, paperWidth: 58 | 80) { + if (!printer.connected) { + alert('Hubungkan printer terlebih dahulu.'); + return; + } + + const rupiah = (n: number) => `Rp ${n.toLocaleString('id-ID')}`; + + const items = (transaction.order_items ?? []).map((item) => ({ + product_name: item.product_variant?.product?.name ?? '-', + variant_name: item.product_variant?.name ?? '-', + quantity: `${Number(item.quantity)}`, + unit_price: rupiah(Number(item.unit_price)), + subtotal: rupiah(Number(item.subtotal)), + })); + + const cashierName = + transaction.created_by?.user_profile?.full_name ?? '-'; + + const data = encodeOrderReceipt( + { + order_number: transaction.order_number, + created_at: format(new Date(transaction.created_at), 'dd/MM/yyyy HH:mm'), + customer_name: transaction.customer?.name ?? null, + cashier_name: cashierName, + items, + subtotal: rupiah(Number(transaction.subtotal)), + discount: rupiah(Number(transaction.discount)), + nego_price: transaction.nego_price != null && Number(transaction.nego_price) > 0 + ? rupiah(Number(transaction.nego_price)) + : null, + total_amount: rupiah(Number(transaction.total_amount)), + notes: transaction.notes ?? null, + }, + { + storeName: appName, + storeAddress: appAddress, + paperWidth, + }, + ); + + printer.print(data).catch((err: Error) => { + alert(`Gagal mencetak: ${err.message}`); + }); + } + const filterToolbar = ( - - - Tambah - - - ) : undefined +
+ {printer.connected ? ( + + + + + + { + const name = await printer.disconnect(); + toast.success('Printer diputuskan.'); + }} + > + + Putuskan + + + + ) : ( + + + + + + { + try { + const name = await printer.connect('bluetooth'); + toast.success(`Terhubung ke ${name}`); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Gagal menghubungkan Bluetooth.'); + } + }} + > + + Hubungkan Bluetooth + + + { + try { + const name = await printer.connect('usb'); + toast.success(`Terhubung ke ${name}`); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Gagal menghubungkan USB.'); + } + }} + > + + Hubungkan USB + + + + )} + + {can('orders.create') ? ( + + ) : undefined} +
} /> @@ -404,6 +523,7 @@ export default function TransactionIndex({ }} onDelete={(t) => setDeleting(t)} onUpdateStatus={(t, status) => handleUpdateStatus(t, status)} + onPrint={(t, pw) => handlePrintReceipt(t, pw)} /> )} renderSubContent={(transaction) => ( diff --git a/resources/js/pages/admin/manage/transaction/transaction-card.tsx b/resources/js/pages/admin/manage/transaction/transaction-card.tsx index 786f2fa..2d690b1 100644 --- a/resources/js/pages/admin/manage/transaction/transaction-card.tsx +++ b/resources/js/pages/admin/manage/transaction/transaction-card.tsx @@ -2,10 +2,17 @@ import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { useCan } from '@/hooks/use-can'; +import type { PaperWidth } from '@/hooks/use-thermal-printer'; import { formatDateTime, formatNumber } from '@/lib/format'; import { formatCurrency } from '@/lib/utils'; -import { CheckCircle, ChevronDown, Pencil, Send, Trash2, XCircle } from 'lucide-react'; +import { CheckCircle, ChevronDown, Pencil, Printer, Send, Trash2, XCircle } from 'lucide-react'; import type { Transaction } from './columns'; const STATUS_BADGE_CLASSES: Record = { @@ -24,6 +31,7 @@ export type TransactionCardRowParams = { onEdit: (transaction: Transaction) => void; onDelete: (transaction: Transaction) => void; onUpdateStatus: (transaction: Transaction, status: string) => void; + onPrint: (transaction: Transaction, paperWidth: PaperWidth) => void; }; export function TransactionCardRow({ @@ -34,6 +42,7 @@ export function TransactionCardRow({ onEdit, onDelete, onUpdateStatus, + onPrint, }: TransactionCardRowParams) { const { can } = useCan(); const items = transaction.order_items ?? []; @@ -51,7 +60,6 @@ export function TransactionCardRow({ ); const statusBadgeClass = STATUS_BADGE_CLASSES[transaction.status] ?? STATUS_BADGE_CLASSES.pending; -console.log('transaction', transaction); return ( @@ -188,7 +196,26 @@ console.log('transaction', transaction); - {(can('orders.update') || can('orders.delete')) && ( +
+ + + + + + onPrint(transaction, 58)} + > + Struk 58mm + + onPrint(transaction, 80)} + > + Struk 80mm + + + - )} +
diff --git a/resources/js/pages/admin/master/product/index.tsx b/resources/js/pages/admin/master/product/index.tsx index cc14f63..2b3f56a 100644 --- a/resources/js/pages/admin/master/product/index.tsx +++ b/resources/js/pages/admin/master/product/index.tsx @@ -34,7 +34,7 @@ import { edit as variantEdit, } from '@/routes/admin/master/products/variants'; import { Head } from '@inertiajs/react'; -import { Plus } from 'lucide-react'; +import { Plus } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { Product, ProductVariant } from './columns'; import { ProductCardRow } from './product-card'; diff --git a/resources/js/types/receipt-printer-encoder.d.ts b/resources/js/types/receipt-printer-encoder.d.ts new file mode 100644 index 0000000..36b7370 --- /dev/null +++ b/resources/js/types/receipt-printer-encoder.d.ts @@ -0,0 +1,35 @@ +declare module '@point-of-sale/receipt-printer-encoder' { + interface ReceiptPrinterEncoderOptions { + language?: string; + codepageMapping?: Record; + columns?: number; + } + + interface TableOptions { + width: number; + align: 'left' | 'center' | 'right'; + } + + type TableCell = string | ((encoder: ReceiptPrinterEncoder) => void); + + class ReceiptPrinterEncoder { + constructor(options?: ReceiptPrinterEncoderOptions); + initialize(): this; + reset(): this; + align(align: 'left' | 'center' | 'right'): this; + bold(bold?: boolean): this; + underline(underline?: boolean): this; + invert(invert?: boolean): this; + width(width: number): this; + height(height: number): this; + text(text: string): this; + newline(count?: number): this; + line(text: string): this; + table(options: TableOptions[], rows: TableCell[][]): this; + cut(): this; + feed(lines?: number): this; + encode(): Uint8Array; + } + + export default ReceiptPrinterEncoder; +} diff --git a/resources/js/types/web-bluetooth-usb.d.ts b/resources/js/types/web-bluetooth-usb.d.ts new file mode 100644 index 0000000..029f851 --- /dev/null +++ b/resources/js/types/web-bluetooth-usb.d.ts @@ -0,0 +1,174 @@ +interface BluetoothDevice { + readonly id: string; + readonly name: string | null; + readonly gatt?: BluetoothRemoteGATTServer; + addEventListener( + type: 'gattserverdisconnected', + listener: EventListener, + ): void; + removeEventListener( + type: 'gattserverdisconnected', + listener: EventListener, + ): void; +} + +interface BluetoothRemoteGATTServer { + readonly device: BluetoothDevice; + readonly connected: boolean; + connect(): Promise; + disconnect(): void; + getPrimaryService( + service: string, + ): Promise; + getPrimaryServices(): Promise; +} + +interface BluetoothRemoteGATTService { + readonly device: BluetoothDevice; + readonly uuid: string; + getCharacteristic( + characteristic: string, + ): Promise; + getCharacteristics(): Promise; +} + +interface BluetoothCharacteristic { + readonly uuid: string; + readonly service: BluetoothRemoteGATTService; + readonly properties: BluetoothCharacteristicProperties; + readValue(): Promise; + writeValueWithResponse(value: BufferSource): Promise; + writeValueWithoutResponse(value: BufferSource): Promise; + startNotifications(): Promise; + stopNotifications(): Promise; +} + +interface BluetoothCharacteristicProperties { + readonly broadcast: boolean; + readonly read: boolean; + readonly writeWithoutResponse: boolean; + readonly write: boolean; + readonly notify: boolean; + readonly indicate: boolean; + readonly authenticatedSignedWrites: boolean; + readonly reliableWrite: boolean; + readonly writableAuxiliaries: boolean; +} + +interface BluetoothRequestDeviceOptions { + filters?: BluetoothLEScanFilter[]; + optionalServices?: string[]; + acceptAllDevices?: boolean; +} + +interface BluetoothLEScanFilter { + services?: string[]; + name?: string; + namePrefix?: string; + manufacturerData?: BluetoothManufacturerDataFilter[]; + serviceData?: BluetoothServiceDataFilter[]; +} + +interface BluetoothManufacturerDataFilter { + companyIdentifier: number; + dataPrefix?: BufferSource; + mask?: BufferSource; +} + +interface BluetoothServiceDataFilter { + service: string; + dataPrefix?: BufferSource; + mask?: BufferSource; +} + +interface Navigator { + bluetooth?: { + requestDevice( + options: BluetoothRequestDeviceOptions, + ): Promise; + }; +} + +interface USBDevice { + readonly vendorId: number; + readonly productId: number; + readonly deviceVersionMajor: number; + readonly deviceVersionMinor: number; + readonly deviceVersionSubminor: number; + readonly manufacturerName: string | null; + readonly productName: string | null; + readonly serialNumber: string | null; + readonly configuration: USBConfiguration | null; + open(): Promise; + close(): Promise; + selectConfiguration(configurationValue: number): Promise; + claimInterface(interfaceNumber: number): Promise; + releaseInterface(interfaceNumber: number): Promise; + transferOut( + endpointNumber: number, + data: BufferSource, + ): Promise; + transferIn( + endpointNumber: number, + length: number, + ): Promise; +} + +interface USBConfiguration { + readonly configurationValue: number; + readonly configurationName: string | null; + readonly interfaces: USBInterface[]; +} + +interface USBInterface { + readonly interfaceNumber: number; + readonly alternate: USBAlternateInterface; + readonly alternates: USBAlternateInterface[]; +} + +interface USBAlternateInterface { + readonly alternateSetting: number; + readonly interfaceClass: number; + readonly interfaceSubclass: number; + readonly interfaceProtocol: number; + readonly interfaceName: string | null; + readonly endpoints: USBEndpoint[]; +} + +interface USBEndpoint { + readonly endpointNumber: number; + readonly direction: 'in' | 'out'; + readonly type: 'bulk' | 'interrupt' | 'isochronous' | 'control'; + readonly packetSize: number; +} + +interface USBOutTransferResult { + readonly bytesWritten: number; + readonly status: 'ok' | 'stall' | 'babble'; +} + +interface USBInTransferResult { + readonly data: DataView | null; + readonly status: 'ok' | 'stall' | 'babble'; +} + +interface Navigator { + usb?: { + requestDevice( + options: USBRequestDeviceOptions, + ): Promise; + }; +} + +interface USBRequestDeviceOptions { + filters: USBDeviceFilter[]; +} + +interface USBDeviceFilter { + vendorId?: number; + productId?: number; + classCode?: number; + subclassCode?: number; + protocolCode?: number; + serialNumber?: string; +} diff --git a/vite.config.ts b/vite.config.ts index ec67f1c..0f2810a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,9 @@ import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; export default defineConfig({ + optimizeDeps: { + include: ['@point-of-sale/receipt-printer-encoder'], + }, plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'],