[Update] print label
Library niimblue tidak digunakan, namun potongan kode tetap disimpan
This commit is contained in:
parent
c8559d63df
commit
8665584567
717
Documentation/NiimbotPrinter-FlowChart.md
Normal file
717
Documentation/NiimbotPrinter-FlowChart.md
Normal file
@ -0,0 +1,717 @@
|
|||||||
|
# 📊 Diagram Alur Kerja Printer Niimbot
|
||||||
|
|
||||||
|
## 🔄 Alur Lengkap: Dari UI ke Printer
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[User buka halaman Brankas] --> B{Printer sudah<br/>terhubung?}
|
||||||
|
B -->|Tidak| C[Klik 'Hubungkan Printer']
|
||||||
|
B -->|Ya| D[Status: Terhubung]
|
||||||
|
|
||||||
|
C --> E[Modal NiimbotConnector muncul]
|
||||||
|
E --> F{Pilih metode koneksi}
|
||||||
|
F -->|Bluetooth| G[Klik 'Hubungkan Printer']
|
||||||
|
F -->|USB/Serial| G
|
||||||
|
|
||||||
|
G --> H[Browser: Dialog pairing muncul]
|
||||||
|
H --> I[User pilih Niimbot device]
|
||||||
|
I --> J[Library: initClient & connect]
|
||||||
|
|
||||||
|
J --> K{Koneksi<br/>berhasil?}
|
||||||
|
K -->|Tidak| L[Tampilkan error]
|
||||||
|
K -->|Ya| M[Fetch printer info]
|
||||||
|
|
||||||
|
M --> N[Status: Terhubung ✓]
|
||||||
|
N --> D
|
||||||
|
|
||||||
|
D --> O[User klik item di Brankas]
|
||||||
|
O --> P[Modal item muncul]
|
||||||
|
P --> Q[Generate QR Code URL]
|
||||||
|
Q --> R[Tampilkan preview QR]
|
||||||
|
|
||||||
|
R --> S[User klik 'Cetak ke Niimbot']
|
||||||
|
S --> T{Printer<br/>terhubung?}
|
||||||
|
|
||||||
|
T -->|Tidak| U[Alert: Hubungkan printer]
|
||||||
|
U --> C
|
||||||
|
|
||||||
|
T -->|Ya| V[createQRLabelCanvas]
|
||||||
|
V --> W[Load QR image]
|
||||||
|
W --> X[Draw ke canvas:<br/>QR + Kode + Nama + Berat]
|
||||||
|
X --> Y[Convert canvas to DataURL]
|
||||||
|
|
||||||
|
Y --> Z[printQRCode composable]
|
||||||
|
Z --> AA[Stop heartbeat]
|
||||||
|
AA --> AB[Create PrintTask]
|
||||||
|
AB --> AC[ImageEncoder.encodeCanvas]
|
||||||
|
AC --> AD[printTask.printInit]
|
||||||
|
AD --> AE[printTask.printPage]
|
||||||
|
|
||||||
|
AE --> AF{Print<br/>sukses?}
|
||||||
|
AF -->|Tidak| AG[Tampilkan error]
|
||||||
|
AF -->|Ya| AH[printTask.printEnd]
|
||||||
|
|
||||||
|
AH --> AI[Start heartbeat]
|
||||||
|
AI --> AJ[Alert: Berhasil dicetak!]
|
||||||
|
AJ --> AK[Printer cetak label]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arsitektur Komponen
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BrankasList.vue │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ UI Layer │ │
|
||||||
|
│ │ - Tombol "Hubungkan Printer" │ │
|
||||||
|
│ │ - Tombol "Cetak ke Niimbot" │ │
|
||||||
|
│ │ - Modal item dengan QR Code │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Logic Layer │ │
|
||||||
|
│ │ - printQR() → Trigger print │ │
|
||||||
|
│ │ - createQRLabelCanvas() → Generate canvas dengan QR │ │
|
||||||
|
│ │ - handlePrinterConnected() → Event handler │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ useNiimbotPrinter.js (Composable) │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ State Management │ │
|
||||||
|
│ │ - printerClient (reactive) │ │
|
||||||
|
│ │ - connectionState │ │
|
||||||
|
│ │ - isPrinting, printProgress │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Methods │ │
|
||||||
|
│ │ - initClient() → Buat NiimbotClient │ │
|
||||||
|
│ │ - connect() → Hubungkan ke printer │ │
|
||||||
|
│ │ - disconnect() → Putuskan koneksi │ │
|
||||||
|
│ │ - printQRCode() → Print image ke printer │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ @mmote/niimbluelib (Library) │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ NiimbotBluetoothClient / NiimbotSerialClient │ │
|
||||||
|
│ │ - connect() → Web Bluetooth/Serial API │ │
|
||||||
|
│ │ - fetchPrinterInfo() → Get printer metadata │ │
|
||||||
|
│ │ - startHeartbeat() → Maintain connection │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PrintTask │ │
|
||||||
|
│ │ - newPrintTask() → Buat task print │ │
|
||||||
|
│ │ - printInit() → Inisialisasi print │ │
|
||||||
|
│ │ - printPage() → Kirim data gambar │ │
|
||||||
|
│ │ - waitForFinished() → Tunggu selesai │ │
|
||||||
|
│ │ - printEnd() → Akhiri print │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ImageEncoder │ │
|
||||||
|
│ │ - encodeCanvas() → Encode canvas ke binary │ │
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Browser Web APIs │
|
||||||
|
│ - Web Bluetooth API (navigator.bluetooth) │
|
||||||
|
│ - Web Serial API (navigator.serial) │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Printer Niimbot (Hardware) │
|
||||||
|
│ - Terima perintah via BLE/USB │
|
||||||
|
│ - Decode binary image data │
|
||||||
|
│ - Print ke label thermal │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Sequence Diagram: Proses Print
|
||||||
|
|
||||||
|
```
|
||||||
|
User BrankasList useNiimbot niimbluelib Browser API Printer
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Klik item │ │ │ │ │
|
||||||
|
├───────────────>│ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Modal muncul │ │ │ │ │
|
||||||
|
│<───────────────┤ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Klik 'Cetak' │ │ │ │ │
|
||||||
|
├───────────────>│ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ printQRCode()│ │ │ │
|
||||||
|
│ ├─────────────>│ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ stopHeartbeat() │ │
|
||||||
|
│ │ ├─────────────>│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ newPrintTask() │ │
|
||||||
|
│ │ ├─────────────>│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ encodeCanvas() │ │
|
||||||
|
│ │ ├─────────────>│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ BLE/Serial │ │
|
||||||
|
│ │ │ │ Write Data │ │
|
||||||
|
│ │ │ ├─────────────>│ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ Send Data │
|
||||||
|
│ │ │ │ ├───────────>│
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ Printing │
|
||||||
|
│ │ │ │ │ ........ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ Status ACK │ │
|
||||||
|
│ │ │ │<─────────────┤ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ printEnd() │ │ │
|
||||||
|
│ │ ├─────────────>│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ startHeartbeat() │ │
|
||||||
|
│ │ ├─────────────>│ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ Alert sukses │ │ │ │
|
||||||
|
│ │<─────────────┤ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ Label tercetak│ │ │ │ │
|
||||||
|
│<───────────────┴──────────────┴──────────────┴──────────────┴────────────┤
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Connection Flow Detail
|
||||||
|
|
||||||
|
### Bluetooth Connection
|
||||||
|
```
|
||||||
|
1. User Action
|
||||||
|
└─> Klik "Hubungkan Printer"
|
||||||
|
|
||||||
|
2. initClient('bluetooth')
|
||||||
|
└─> new NiimbotBluetoothClient()
|
||||||
|
|
||||||
|
3. connect()
|
||||||
|
└─> navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ namePrefix: 'Niimbot' }],
|
||||||
|
optionalServices: [SERVICE_UUID]
|
||||||
|
})
|
||||||
|
|
||||||
|
4. Browser Dialog
|
||||||
|
└─> User pilih device "Niimbot-XXXX"
|
||||||
|
|
||||||
|
5. device.gatt.connect()
|
||||||
|
└─> Establish BLE connection
|
||||||
|
|
||||||
|
6. getPrimaryService(SERVICE_UUID)
|
||||||
|
└─> getCharacteristic(TX_CHAR, RX_CHAR)
|
||||||
|
|
||||||
|
7. Event: 'connect' triggered
|
||||||
|
└─> connectionState = 'connected'
|
||||||
|
|
||||||
|
8. fetchPrinterInfo()
|
||||||
|
└─> Send command: GET_INFO
|
||||||
|
└─> Receive: model, serial, firmware version
|
||||||
|
|
||||||
|
9. startHeartbeat()
|
||||||
|
└─> Kirim ping setiap 1 detik
|
||||||
|
└─> Cek printer masih hidup
|
||||||
|
```
|
||||||
|
|
||||||
|
### USB/Serial Connection
|
||||||
|
```
|
||||||
|
1. User Action
|
||||||
|
└─> Klik "Hubungkan Printer"
|
||||||
|
|
||||||
|
2. initClient('serial')
|
||||||
|
└─> new NiimbotSerialClient()
|
||||||
|
|
||||||
|
3. connect()
|
||||||
|
└─> navigator.serial.requestPort({
|
||||||
|
filters: [{ usbVendorId: 0xXXXX }]
|
||||||
|
})
|
||||||
|
|
||||||
|
4. Browser Dialog
|
||||||
|
└─> User pilih USB device
|
||||||
|
|
||||||
|
5. port.open({ baudRate: 9600 })
|
||||||
|
└─> Establish serial connection
|
||||||
|
|
||||||
|
6. Setup reader/writer streams
|
||||||
|
└─> readable.getReader()
|
||||||
|
└─> writable.getWriter()
|
||||||
|
|
||||||
|
7. Event: 'connect' triggered
|
||||||
|
└─> connectionState = 'connected'
|
||||||
|
|
||||||
|
8. fetchPrinterInfo() & startHeartbeat()
|
||||||
|
└─> (sama dengan Bluetooth)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼️ Image Processing Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
QR Code URL (dari API)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
new Image()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
img.onload
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Create Canvas (384x384)
|
||||||
|
│
|
||||||
|
├─> Fill white background
|
||||||
|
│
|
||||||
|
├─> drawImage(qr, x, y, size, size)
|
||||||
|
│
|
||||||
|
├─> fillText(kode_item)
|
||||||
|
│
|
||||||
|
├─> fillText(nama_produk)
|
||||||
|
│
|
||||||
|
├─> fillText(berat)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
canvas.toDataURL('image/png')
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
loadImageToCanvas(dataUrl)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[Optional] applyThreshold(canvas, 140)
|
||||||
|
│
|
||||||
|
├─> getImageData()
|
||||||
|
│
|
||||||
|
├─> for each pixel:
|
||||||
|
│ avg = (r+g+b)/3
|
||||||
|
│ if avg < 140: black
|
||||||
|
│ else: white
|
||||||
|
│
|
||||||
|
├─> putImageData()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ImageEncoder.encodeCanvas(canvas, 'top')
|
||||||
|
│
|
||||||
|
├─> Convert to 1-bit bitmap
|
||||||
|
│
|
||||||
|
├─> Rotate if needed (printDirection)
|
||||||
|
│
|
||||||
|
├─> Pack bits: 8 pixels = 1 byte
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Binary Data (EncodedImage)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
printTask.printPage(encoded, quantity)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Send to Printer via BLE/Serial
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 State Machine Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ DISCONNECTED │◄────────┐
|
||||||
|
└────────┬────────┘ │
|
||||||
|
│ │
|
||||||
|
│ connect() │ disconnect()
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────┐ │
|
||||||
|
│ CONNECTING │ │
|
||||||
|
└────────┬────────┘ │
|
||||||
|
│ │
|
||||||
|
│ success │ error
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────┐ │
|
||||||
|
│ CONNECTED │─────────┤
|
||||||
|
└────────┬────────┘ │
|
||||||
|
│ │
|
||||||
|
│ printQRCode() │
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────┐ │
|
||||||
|
│ PRINTING │ │
|
||||||
|
│ [progress: X%] │ │
|
||||||
|
└────────┬────────┘ │
|
||||||
|
│ │
|
||||||
|
│ finished │
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────┐ │
|
||||||
|
│ CONNECTED │─────────┘
|
||||||
|
│ (heartbeat) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Data Flow: QR Code → Printed Label
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. DATA SOURCE │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ selectedItem = { │
|
||||||
|
│ kode_item: "BRN-001", │
|
||||||
|
│ produk: { │
|
||||||
|
│ nama: "Cincin Emas 24K", │
|
||||||
|
│ berat: 5.2 │
|
||||||
|
│ } │
|
||||||
|
│ } │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 2. QR CODE GENERATION (External API) │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ URL: https://api.qrserver.com/v1/create-qr-code/ │
|
||||||
|
│ ?size=150x150&data=BRN-001 │
|
||||||
|
│ │
|
||||||
|
│ Returns: image/png (base64 or URL) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 3. CANVAS CREATION │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ createQRLabelCanvas(qrUrl, item) │
|
||||||
|
│ │
|
||||||
|
│ Canvas 384x384px: │
|
||||||
|
│ ┌──────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────┐ │ ← QR Code (200x200) │
|
||||||
|
│ │ │ ▓▓ ▓▓▓▓│ │ │
|
||||||
|
│ │ │▓ ▓▓ ▓ │ │ │
|
||||||
|
│ │ │ ▓▓▓ ▓▓▓│ │ │
|
||||||
|
│ │ └──────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ BRN-001 │ ← Kode Item (bold 18px) │
|
||||||
|
│ │ Cincin Emas 24K │ ← Nama Produk (14px) │
|
||||||
|
│ │ 5.2g │ ← Berat (12px) │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────┘ │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 4. IMAGE ENCODING │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ ImageEncoder.encodeCanvas(canvas, 'top') │
|
||||||
|
│ │
|
||||||
|
│ Process: │
|
||||||
|
│ - Convert RGB to grayscale │
|
||||||
|
│ - Apply threshold (< 140 = black, >= 140 = white) │
|
||||||
|
│ - Pack 8 pixels into 1 byte (1-bit bitmap) │
|
||||||
|
│ - Width: 384px = 48 bytes per row │
|
||||||
|
│ - Height: 384 rows │
|
||||||
|
│ - Total: 48 × 384 = 18,432 bytes │
|
||||||
|
│ │
|
||||||
|
│ Output: Uint8Array(18432) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 5. PRINT PROTOCOL │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ printTask.printInit() │
|
||||||
|
│ └─> Send: [CMD_INIT, density, labelType, ...] │
|
||||||
|
│ │
|
||||||
|
│ printTask.printPage(encoded, quantity) │
|
||||||
|
│ └─> Send in chunks: │
|
||||||
|
│ [CMD_IMAGE_HEADER, width, height] │
|
||||||
|
│ [CMD_IMAGE_DATA, chunk1...] │
|
||||||
|
│ [CMD_IMAGE_DATA, chunk2...] │
|
||||||
|
│ ... │
|
||||||
|
│ [CMD_IMAGE_END] │
|
||||||
|
│ │
|
||||||
|
│ printTask.waitForFinished() │
|
||||||
|
│ └─> Poll status every 100ms │
|
||||||
|
│ until status = FINISHED │
|
||||||
|
│ │
|
||||||
|
│ printTask.printEnd() │
|
||||||
|
│ └─> Send: [CMD_PRINT_END] │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ 6. PHYSICAL OUTPUT │
|
||||||
|
│ ─────────────────────────────────────────────────────────│
|
||||||
|
│ Printer Niimbot: │
|
||||||
|
│ - Receives binary data via BLE/USB │
|
||||||
|
│ - Thermal head heats selected pixels │
|
||||||
|
│ - Paper feeds forward │
|
||||||
|
│ - Label printed with QR + text │
|
||||||
|
│ - Cut (automatic or manual) │
|
||||||
|
│ │
|
||||||
|
│ Result: Physical label 40x40mm dengan QR code │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Component Interaction Map
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ App.vue / Router │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ BrankasList.vue │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────┐ │
|
||||||
|
│ │ Header │ │
|
||||||
|
│ │ - Printer Btn │──┼──┐
|
||||||
|
│ └────────────────┘ │ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────┐ │ │
|
||||||
|
│ │ Item List │ │ │
|
||||||
|
│ │ - Click item │──┼──┼──┐
|
||||||
|
│ └────────────────┘ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌────────────────┐ │ │ │
|
||||||
|
│ │ Item Modal │ │ │ │
|
||||||
|
│ │ - QR Preview │ │ │ │
|
||||||
|
│ │ - Print Btn │──┼──┼──┼──┐
|
||||||
|
│ └────────────────┘ │ │ │ │
|
||||||
|
└──────────────────────┘ │ │ │
|
||||||
|
│ │ │
|
||||||
|
┌─────────────────────────────────────┘ │ │
|
||||||
|
│ │ │
|
||||||
|
▼ │ │
|
||||||
|
┌──────────────────┐ │ │
|
||||||
|
│ NiimbotConnector │ │ │
|
||||||
|
│ .vue │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ - Connect UI │◄───────────────────────────┘ │
|
||||||
|
│ - Status display │ │
|
||||||
|
│ - Printer info │ │
|
||||||
|
└────────┬─────────┘ │
|
||||||
|
│ │
|
||||||
|
│ uses │
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
┌──────────────────────────────────────────────────┼───┐
|
||||||
|
│ useNiimbotPrinter.js (Composable) │ │
|
||||||
|
│ │ │
|
||||||
|
│ - State: printerClient, connectionState, etc │◄──┘
|
||||||
|
│ - Methods: connect(), disconnect(), printQR() │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────┐ │
|
||||||
|
│ │ Event Listeners Setup │ │
|
||||||
|
│ │ - on('connect') │ │
|
||||||
|
│ │ - on('disconnect') │ │
|
||||||
|
│ │ - on('printprogress') │ │
|
||||||
|
│ │ - on('heartbeat') │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────┬───────────────────────────────┘
|
||||||
|
│ imports
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────┐
|
||||||
|
│ @mmote/niimbluelib │
|
||||||
|
│ │
|
||||||
|
│ - NiimbotBluetoothClient │
|
||||||
|
│ - NiimbotSerialClient │
|
||||||
|
│ - ImageEncoder │
|
||||||
|
│ - PrintTask abstraction │
|
||||||
|
│ - Utils │
|
||||||
|
└──────────────────┬─────────────────────────────────┘
|
||||||
|
│ uses
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────┐
|
||||||
|
│ Browser APIs │
|
||||||
|
│ - navigator.bluetooth (Web Bluetooth API) │
|
||||||
|
│ - navigator.serial (Web Serial API) │
|
||||||
|
└────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance Timeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Event Timeline (typical print job):
|
||||||
|
|
||||||
|
0ms │ User clicks "Cetak ke Niimbot"
|
||||||
|
│
|
||||||
|
10ms │ Check printer connection
|
||||||
|
│ └─> Already connected ✓
|
||||||
|
│
|
||||||
|
20ms │ createQRLabelCanvas() start
|
||||||
|
│ └─> Load QR image
|
||||||
|
│
|
||||||
|
150ms │ Image loaded, draw to canvas
|
||||||
|
│ └─> Draw QR, text, etc
|
||||||
|
│
|
||||||
|
170ms │ canvas.toDataURL()
|
||||||
|
│
|
||||||
|
200ms │ printQRCode() called
|
||||||
|
│ └─> stopHeartbeat()
|
||||||
|
│
|
||||||
|
220ms │ newPrintTask()
|
||||||
|
│
|
||||||
|
240ms │ ImageEncoder.encodeCanvas()
|
||||||
|
│ └─> RGB → grayscale
|
||||||
|
│ └─> Threshold
|
||||||
|
│ └─> Pack to 1-bit
|
||||||
|
│
|
||||||
|
450ms │ Encoding complete (18KB data)
|
||||||
|
│
|
||||||
|
470ms │ printTask.printInit()
|
||||||
|
│ └─> Send init command
|
||||||
|
│
|
||||||
|
490ms │ ACK received
|
||||||
|
│
|
||||||
|
500ms │ printTask.printPage()
|
||||||
|
│ └─> Send image header
|
||||||
|
│
|
||||||
|
520ms │ Send data chunk 1/10
|
||||||
|
540ms │ Send data chunk 2/10
|
||||||
|
... │ ...
|
||||||
|
740ms │ Send data chunk 10/10
|
||||||
|
│
|
||||||
|
750ms │ All data sent
|
||||||
|
│
|
||||||
|
780ms │ waitForFinished() polling start
|
||||||
|
│
|
||||||
|
800ms │ Status: PRINTING (0%)
|
||||||
|
900ms │ Status: PRINTING (25%)
|
||||||
|
1000ms │ Status: PRINTING (50%)
|
||||||
|
1100ms │ Status: PRINTING (75%)
|
||||||
|
1200ms │ Status: PRINTING (100%)
|
||||||
|
│
|
||||||
|
1220ms │ Status: FINISHED
|
||||||
|
│
|
||||||
|
1230ms │ printTask.printEnd()
|
||||||
|
│
|
||||||
|
1250ms │ startHeartbeat()
|
||||||
|
│
|
||||||
|
1260ms │ Alert: "QR Code berhasil dicetak!"
|
||||||
|
│
|
||||||
|
Total: ~1.3 seconds dari klik hingga selesai
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security & Privacy
|
||||||
|
|
||||||
|
```
|
||||||
|
Data Flow Security:
|
||||||
|
|
||||||
|
┌──────────────────┐
|
||||||
|
│ User's Browser │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
│ HTTPS (encrypted)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ QR API Server │ ← External: api.qrserver.com
|
||||||
|
└────────┬─────────┘ (Kirim kode_item only)
|
||||||
|
│
|
||||||
|
│ Returns image URL
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ User's Browser │
|
||||||
|
│ - Generate │
|
||||||
|
│ canvas │
|
||||||
|
│ - Encode image │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
│ BLE/USB (direct, tidak via internet)
|
||||||
|
│ Encrypted jika BLE
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Niimbot Printer │ ← Local device, tidak terkoneksi internet
|
||||||
|
└──────────────────┘
|
||||||
|
|
||||||
|
Privacy Notes:
|
||||||
|
- Data tidak lewat server backend aplikasi
|
||||||
|
- QR Code dibuat real-time di browser
|
||||||
|
- Gambar langsung dikirim ke printer lokal
|
||||||
|
- Tidak ada logging data item ke cloud
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Error Handling Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
│ User Action │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Try: connect() │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌────────────┴────────────┐
|
||||||
|
│ │
|
||||||
|
┌──────▼──────┐ ┌──────▼──────┐
|
||||||
|
│ Success │ │ Error │
|
||||||
|
└──────┬──────┘ └──────┬──────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ ┌────────────────┐
|
||||||
|
│ │ Catch error │
|
||||||
|
│ │ - Log to │
|
||||||
|
│ │ console │
|
||||||
|
│ │ - Set error │
|
||||||
|
│ │ message │
|
||||||
|
│ │ - Alert user │
|
||||||
|
│ └────────┬───────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ ┌────────────────┐
|
||||||
|
│ │ connectionState│
|
||||||
|
│ │ = 'disconnected│
|
||||||
|
│ └────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────┐
|
||||||
|
│ fetchPrinterInfo│
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌──────┴──────┐
|
||||||
|
│ │
|
||||||
|
┌─────▼─────┐ ┌────▼────┐
|
||||||
|
│ Success │ │ Error │
|
||||||
|
└─────┬─────┘ └────┬────┘
|
||||||
|
│ │
|
||||||
|
│ └──> Log & continue (non-critical)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ startHeartbeat()│
|
||||||
|
└─────────┬───────┘
|
||||||
|
│
|
||||||
|
└──> Connected & Ready
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Semua diagram ini membantu memahami bagaimana sistem bekerja end-to-end! 🎉
|
||||||
373
Documentation/NiimbotPrinter.md
Normal file
373
Documentation/NiimbotPrinter.md
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
# 🖨️ Dokumentasi Integrasi Printer Niimbot
|
||||||
|
|
||||||
|
## 📋 Ringkasan
|
||||||
|
|
||||||
|
Aplikasi kasir sekarang mendukung pencetakan QR Code langsung ke printer Niimbot menggunakan library `@mmote/niimbluelib` yang diadaptasi dari proyek niimblue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Teknologi & Library
|
||||||
|
|
||||||
|
### Library Utama
|
||||||
|
- **@mmote/niimbluelib** - Library JavaScript untuk komunikasi dengan printer Niimbot
|
||||||
|
- Mendukung Web Bluetooth API
|
||||||
|
- Mendukung Web Serial API (USB)
|
||||||
|
- Encoding gambar ke format printer
|
||||||
|
|
||||||
|
### Browser Support
|
||||||
|
| Browser | Bluetooth | USB (Serial) |
|
||||||
|
|---------|-----------|--------------|
|
||||||
|
| Chrome/Edge (Desktop) | ✅ | ✅ |
|
||||||
|
| Chrome (Android) | ✅ | ❌ |
|
||||||
|
| Firefox | ❌ | ❌ |
|
||||||
|
| Safari | ❌ | ❌ |
|
||||||
|
|
||||||
|
**Rekomendasi:** Gunakan Chrome atau Edge versi terbaru
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Struktur File
|
||||||
|
|
||||||
|
```
|
||||||
|
Kasir/resources/js/
|
||||||
|
├── composables/
|
||||||
|
│ └── useNiimbotPrinter.js # Composable untuk koneksi & print
|
||||||
|
├── components/
|
||||||
|
│ ├── NiimbotConnector.vue # Modal untuk koneksi printer
|
||||||
|
│ └── BrankasList.vue # Modifikasi: tambah fitur print Niimbot
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cara Kerja
|
||||||
|
|
||||||
|
### 1️⃣ Alur Koneksi Printer
|
||||||
|
|
||||||
|
```
|
||||||
|
User klik "Hubungkan Printer"
|
||||||
|
→ Modal NiimbotConnector muncul
|
||||||
|
→ Pilih metode: Bluetooth / USB
|
||||||
|
→ Browser munculkan dialog pairing
|
||||||
|
→ User pilih printer Niimbot
|
||||||
|
→ Koneksi terjalin
|
||||||
|
→ Fetch info printer
|
||||||
|
→ Status: "Terhubung"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ Alur Print QR Code
|
||||||
|
|
||||||
|
```
|
||||||
|
User klik item di Brankas
|
||||||
|
→ Modal popup dengan QR Code
|
||||||
|
→ User klik "Cetak ke Niimbot"
|
||||||
|
→ Cek koneksi printer:
|
||||||
|
- Jika belum terhubung → tampilkan modal koneksi
|
||||||
|
- Jika sudah terhubung → lanjut
|
||||||
|
→ Generate canvas dengan QR + info item
|
||||||
|
→ Encode canvas ke format printer
|
||||||
|
→ Kirim ke printer via library
|
||||||
|
→ Status: "Mencetak... X%"
|
||||||
|
→ Selesai
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ Komponen Kunci
|
||||||
|
|
||||||
|
#### `useNiimbotPrinter.js` (Composable)
|
||||||
|
```javascript
|
||||||
|
// State management
|
||||||
|
- printerClient // Instance NiimbotClient
|
||||||
|
- connectionState // 'disconnected' | 'connecting' | 'connected'
|
||||||
|
- connectedPrinterName // Nama printer yang terhubung
|
||||||
|
- printerInfo // Info printer (model, serial, dll)
|
||||||
|
- isPrinting // Status sedang print
|
||||||
|
- printProgress // Progress print (0-100)
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
- initClient(type) // Init client (bluetooth/serial)
|
||||||
|
- connect() // Hubungkan ke printer
|
||||||
|
- disconnect() // Putuskan koneksi
|
||||||
|
- printQRCode(dataUrl, options) // Print QR code image
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `NiimbotConnector.vue` (Component)
|
||||||
|
- Modal untuk koneksi/disconnect printer
|
||||||
|
- Pilih metode: Bluetooth atau USB
|
||||||
|
- Tampilkan status koneksi
|
||||||
|
- Tampilkan info printer detail
|
||||||
|
|
||||||
|
#### `BrankasList.vue` (Modified)
|
||||||
|
- Tombol "Hubungkan Printer" di header
|
||||||
|
- Tombol "Cetak ke Niimbot" di modal item
|
||||||
|
- Tombol "Browser" sebagai fallback
|
||||||
|
- Alert sukses/error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Instalasi
|
||||||
|
|
||||||
|
### 1. Install Library
|
||||||
|
```bash
|
||||||
|
cd "c:\Data\Magang\Toko perhiasan\Kasir"
|
||||||
|
npm install @mmote/niimbluelib
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Files Sudah Dibuat
|
||||||
|
- ✅ `resources/js/composables/useNiimbotPrinter.js`
|
||||||
|
- ✅ `resources/js/components/NiimbotConnector.vue`
|
||||||
|
- ✅ `resources/js/components/BrankasList.vue` (modified)
|
||||||
|
|
||||||
|
### 3. Build Assets
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# atau untuk development
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Cara Penggunaan
|
||||||
|
|
||||||
|
### A. Menghubungkan Printer
|
||||||
|
|
||||||
|
1. **Persiapan Printer:**
|
||||||
|
- Nyalakan printer Niimbot
|
||||||
|
- Pastikan kertas label sudah terpasang
|
||||||
|
- Untuk Bluetooth: Aktifkan mode pairing (biasanya tahan tombol power)
|
||||||
|
|
||||||
|
2. **Di Aplikasi:**
|
||||||
|
- Buka halaman Brankas
|
||||||
|
- Klik tombol "Hubungkan Printer" (di kanan atas)
|
||||||
|
- Pilih metode koneksi (Bluetooth/USB)
|
||||||
|
- Klik "Hubungkan Printer"
|
||||||
|
- Browser akan menampilkan dialog - pilih printer Niimbot Anda
|
||||||
|
- Tunggu hingga status berubah "Terhubung"
|
||||||
|
|
||||||
|
### B. Mencetak QR Code
|
||||||
|
|
||||||
|
1. **Pilih Item:**
|
||||||
|
- Klik item yang ingin dicetak QR-nya
|
||||||
|
- Modal akan muncul dengan QR Code
|
||||||
|
|
||||||
|
2. **Cetak:**
|
||||||
|
- Klik tombol "Cetak ke Niimbot"
|
||||||
|
- Jika printer belum terhubung, akan muncul peringatan
|
||||||
|
- Progress print akan ditampilkan (Mencetak... X%)
|
||||||
|
- Tunggu hingga selesai
|
||||||
|
|
||||||
|
3. **Alternatif:**
|
||||||
|
- Klik tombol "Browser" untuk print menggunakan printer biasa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Konfigurasi Print
|
||||||
|
|
||||||
|
Di file `BrankasList.vue`, fungsi `printQR()`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await printQRCode(imageDataUrl, {
|
||||||
|
density: 3, // Kepadatan tinta (1-5, default: 3)
|
||||||
|
quantity: 1, // Jumlah cetakan (default: 1)
|
||||||
|
labelType: 1, // Tipe label:
|
||||||
|
// 1 = WithGaps (label dengan jarak)
|
||||||
|
// 2 = Continuous (tanpa jarak)
|
||||||
|
// 3 = WithHoles (dengan lubang)
|
||||||
|
printTaskName: 'D110' // Model printer (D110, B21, B1, dll)
|
||||||
|
// Auto-detect jika tidak cocok
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ukuran Label
|
||||||
|
Di fungsi `createQRLabelCanvas()`:
|
||||||
|
```javascript
|
||||||
|
const labelWidth = 384; // pixel (40mm @ 240dpi)
|
||||||
|
const labelHeight = 384; // pixel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sesuaikan dengan ukuran label Anda:**
|
||||||
|
- 30mm x 30mm ≈ 288 x 288 px
|
||||||
|
- 40mm x 40mm ≈ 384 x 384 px
|
||||||
|
- 50mm x 30mm ≈ 480 x 288 px
|
||||||
|
|
||||||
|
Rumus: `mm × (dpi / 25.4)`
|
||||||
|
- DPI printer Niimbot umumnya: 203 atau 240
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### 1. "Browser tidak mendukung koneksi printer"
|
||||||
|
**Solusi:**
|
||||||
|
- Gunakan Chrome atau Edge versi terbaru
|
||||||
|
- Update browser ke versi terkini
|
||||||
|
- Pastikan menggunakan HTTPS (Web Bluetooth hanya jalan di HTTPS)
|
||||||
|
|
||||||
|
### 2. "Gagal terhubung ke printer"
|
||||||
|
**Solusi:**
|
||||||
|
- Pastikan printer sudah dinyalakan
|
||||||
|
- Pastikan Bluetooth/USB aktif
|
||||||
|
- Coba matikan dan nyalakan ulang printer
|
||||||
|
- Untuk Bluetooth: pastikan tidak paired ke device lain
|
||||||
|
- Coba refresh halaman dan ulangi koneksi
|
||||||
|
|
||||||
|
### 3. "Printer terhubung tapi tidak print"
|
||||||
|
**Solusi:**
|
||||||
|
- Periksa kertas label sudah terpasang dengan benar
|
||||||
|
- Coba disconnect dan connect ulang
|
||||||
|
- Periksa battery printer (untuk model portable)
|
||||||
|
- Cek ukuran canvas sesuai dengan ukuran label
|
||||||
|
|
||||||
|
### 4. "Print hasil blur/tidak jelas"
|
||||||
|
**Solusi:**
|
||||||
|
- Tingkatkan `density` (misal dari 3 ke 4)
|
||||||
|
- Pastikan threshold image sudah optimal
|
||||||
|
- Ukuran QR Code jangan terlalu kecil (min 150x150px)
|
||||||
|
|
||||||
|
### 5. "Error: printTaskName not compatible"
|
||||||
|
**Solusi:**
|
||||||
|
- Ganti `printTaskName` sesuai model printer:
|
||||||
|
- D110: `'D110'`
|
||||||
|
- B21: `'B21'`
|
||||||
|
- B1: `'B1'`
|
||||||
|
- Atau hapus parameter, biarkan auto-detect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
### Enable Console Logs
|
||||||
|
Library sudah dilengkapi logging:
|
||||||
|
```javascript
|
||||||
|
// Di browser console akan muncul:
|
||||||
|
>> Packet sent: [hex bytes]
|
||||||
|
<< Packet received: [hex bytes]
|
||||||
|
Printer connected: { deviceName: "Niimbot-XXXX" }
|
||||||
|
Printer info fetched: { model: "D110", ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Print Canvas
|
||||||
|
Tambahkan debug preview sebelum print:
|
||||||
|
```javascript
|
||||||
|
// Di createQRLabelCanvas, sebelum resolve(canvas):
|
||||||
|
document.body.appendChild(canvas); // Tampilkan canvas di halaman
|
||||||
|
canvas.style.border = '1px solid red';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Referensi API
|
||||||
|
|
||||||
|
### useNiimbotPrinter Composable
|
||||||
|
|
||||||
|
#### State (Reactive)
|
||||||
|
```javascript
|
||||||
|
const {
|
||||||
|
printerClient, // NiimbotClient instance
|
||||||
|
connectionState, // 'disconnected' | 'connecting' | 'connected'
|
||||||
|
connectedPrinterName, // string
|
||||||
|
printerInfo, // object { model, serial, ... }
|
||||||
|
printerMeta, // object { densityMin, densityMax, ... }
|
||||||
|
heartbeatData, // object { powerLevel, ... }
|
||||||
|
isPrinting, // boolean
|
||||||
|
printProgress, // number (0-100)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
isConnected, // boolean
|
||||||
|
isDisconnected, // boolean
|
||||||
|
featureSupport, // { webBluetooth, webSerial, ... }
|
||||||
|
} = useNiimbotPrinter();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
```javascript
|
||||||
|
// Init client
|
||||||
|
initClient(type: 'bluetooth' | 'serial'): void
|
||||||
|
|
||||||
|
// Connect/Disconnect
|
||||||
|
connect(): Promise<void>
|
||||||
|
disconnect(): void
|
||||||
|
|
||||||
|
// Print
|
||||||
|
printQRCode(
|
||||||
|
imageDataUrl: string,
|
||||||
|
options?: {
|
||||||
|
density?: number, // 1-5
|
||||||
|
quantity?: number, // jumlah print
|
||||||
|
labelType?: number, // 1=WithGaps, 2=Continuous, 3=WithHoles
|
||||||
|
printTaskName?: string // 'D110', 'B21', dll
|
||||||
|
}
|
||||||
|
): Promise<{ success: boolean, message: string }>
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
loadImageToCanvas(dataUrl: string): Promise<HTMLCanvasElement>
|
||||||
|
applyThreshold(canvas: HTMLCanvasElement, threshold?: number): HTMLCanvasElement
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Ubah Desain Label
|
||||||
|
Edit fungsi `createQRLabelCanvas()` di `BrankasList.vue`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Contoh: Tambah logo toko
|
||||||
|
const logo = new Image();
|
||||||
|
logo.src = '/path/to/logo.png';
|
||||||
|
logo.onload = () => {
|
||||||
|
ctx.drawImage(logo, 10, 10, 50, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Contoh: Tambah border
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(5, 5, labelWidth - 10, labelHeight - 10);
|
||||||
|
|
||||||
|
// Contoh: Font custom
|
||||||
|
ctx.font = 'bold 20px "Courier New"';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tambah Barcode
|
||||||
|
Install library barcode:
|
||||||
|
```bash
|
||||||
|
npm install jsbarcode
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementasi:
|
||||||
|
```javascript
|
||||||
|
import JsBarcode from 'jsbarcode';
|
||||||
|
|
||||||
|
const barcodeCanvas = document.createElement('canvas');
|
||||||
|
JsBarcode(barcodeCanvas, item.kode_item, {
|
||||||
|
format: 'CODE128',
|
||||||
|
width: 2,
|
||||||
|
height: 40,
|
||||||
|
});
|
||||||
|
ctx.drawImage(barcodeCanvas, x, y);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Catatan Penting
|
||||||
|
|
||||||
|
1. **HTTPS Required**: Web Bluetooth & Web Serial hanya bekerja di HTTPS (kecuali localhost)
|
||||||
|
2. **User Gesture**: Koneksi harus dipicu oleh user action (klik button), tidak bisa otomatis on load
|
||||||
|
3. **One Printer**: Satu browser session hanya bisa terhubung ke 1 printer
|
||||||
|
4. **Battery**: Printer portable akan disconnect otomatis jika battery rendah
|
||||||
|
5. **Label Size**: Sesuaikan ukuran canvas dengan ukuran label fisik untuk hasil optimal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
Jika ada masalah:
|
||||||
|
1. Periksa console browser (F12 → Console)
|
||||||
|
2. Periksa kompatibilitas browser
|
||||||
|
3. Periksa dokumentasi printer Niimbot Anda
|
||||||
|
4. Issue library: https://github.com/MultiMote/niimbluelib
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Menggunakan library `@mmote/niimbluelib` yang bersifat open-source.
|
||||||
|
Pastikan mematuhi lisensi library saat deploy production.
|
||||||
BIN
driver/NiimbotPrinterDriverInstall_3.0.0.5.exe
Normal file
BIN
driver/NiimbotPrinterDriverInstall_3.0.0.5.exe
Normal file
Binary file not shown.
24
package-lock.json
generated
24
package-lock.json
generated
@ -1874,9 +1874,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.11.0",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3700,14 +3700,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@ -3805,9 +3805,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
|
||||||
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3816,7 +3816,7 @@
|
|||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@ -7,11 +7,13 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Alert Section -->
|
<!-- Alert Section -->
|
||||||
<div class="mb-4" v-if="alert">
|
<div class="mb-4" v-if="alert">
|
||||||
<div v-if="alert.error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
<div v-if="alert.error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4"
|
||||||
|
role="alert">
|
||||||
<strong class="font-bold">Error!</strong>
|
<strong class="font-bold">Error!</strong>
|
||||||
<span class="block sm:inline">{{ alert.error }}</span>
|
<span class="block sm:inline">{{ alert.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="alert.success" class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert">
|
<div v-if="alert.success"
|
||||||
|
class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert">
|
||||||
<strong class="font-bold">Success!</strong>
|
<strong class="font-bold">Success!</strong>
|
||||||
<span class="block sm:inline">{{ alert.success }}</span>
|
<span class="block sm:inline">{{ alert.success }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -46,8 +48,7 @@
|
|||||||
@click="openMovePopup(item)">
|
@click="openMovePopup(item)">
|
||||||
<!-- Gambar & Info Produk -->
|
<!-- Gambar & Info Produk -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<img v-if="item.produk?.foto?.length" :src="item.produk?.foto[0].url"
|
<img v-if="item.produk?.foto?.length" :src="item.produk?.foto[0].url" class="size-12 object-cover rounded"
|
||||||
class="size-12 object-cover rounded"
|
|
||||||
@error="handleImageError" />
|
@error="handleImageError" />
|
||||||
<div class="size-12 bg-gray-200 rounded flex items-center justify-center" v-else>
|
<div class="size-12 bg-gray-200 rounded flex items-center justify-center" v-else>
|
||||||
<i class="fas fa-image text-gray-400"></i>
|
<i class="fas fa-image text-gray-400"></i>
|
||||||
@ -64,8 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Pindah Nampan -->
|
<!-- Modal Pindah Nampan -->
|
||||||
<div v-if="isPopupVisible" class="fixed inset-0 bg-black/75 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
|
<div v-if="isPopupVisible"
|
||||||
<div class="bg-white rounded-xl shadow-lg max-w-sm w-full p-6 relative transform transition-all duration-300 scale-95 opacity-0 animate-fadeIn">
|
class="fixed inset-0 bg-black/75 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-xl shadow-lg max-w-sm w-full p-6 relative transform transition-all duration-300 scale-95 opacity-0 animate-fadeIn">
|
||||||
<!-- QR Code -->
|
<!-- QR Code -->
|
||||||
<div class="flex justify-center mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
<div class="p-2 border border-C rounded-lg">
|
<div class="p-2 border border-C rounded-lg">
|
||||||
@ -85,9 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tombol Cetak -->
|
<!-- Tombol Cetak -->
|
||||||
<div class="flex justify-center mb-4">
|
<div class="flex justify-center gap-2 mb-4">
|
||||||
<button @click="printQR" class="bg-D text-A px-4 py-2 rounded hover:bg-D/80 transition">
|
<button @click="printQR" class="bg-D text-A px-4 py-2 rounded hover:bg-D/80 transition"
|
||||||
<i class="fas fa-print mr-2"></i>Cetak
|
title="Cetak menggunakan printer browser">
|
||||||
|
<i class="fas fa-print mr-2"></i>
|
||||||
|
Print
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -107,9 +112,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tombol -->
|
<!-- Tombol -->
|
||||||
<!-- Tombol -->
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button @click="closePopup" class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50 transition">
|
<button @click="closePopup"
|
||||||
|
class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50 transition">
|
||||||
Batal
|
Batal
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -125,23 +130,19 @@
|
|||||||
{{ isMoving ? 'Memindahkan...' : 'Pindahkan' }}
|
{{ isMoving ? 'Memindahkan...' : 'Pindahkan' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Modal Konfirmasi Hapus -->
|
<!-- Modal Konfirmasi Hapus -->
|
||||||
<ConfirmDeleteModal
|
<ConfirmDeleteModal :isOpen="showDeleteConfirm" title="Konfirmasi Hapus Item"
|
||||||
:isOpen="showDeleteConfirm"
|
message="Apakah kamu yakin ingin menghapus item ini?" confirmText="Ya, Hapus" cancelText="Batal"
|
||||||
title="Konfirmasi Hapus Item"
|
@confirm="confirmDelete" @cancel="cancelDelete" />
|
||||||
message="Apakah kamu yakin ingin menghapus item ini?"
|
|
||||||
confirmText="Ya, Hapus"
|
|
||||||
cancelText="Batal"
|
|
||||||
@confirm="confirmDelete"
|
|
||||||
@cancel="cancelDelete"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Confirm Modal untuk aksi berbahaya (jika diperlukan di masa depan) -->
|
<!-- Confirm Modal untuk aksi berbahaya (jika diperlukan di masa depan) -->
|
||||||
<div v-if="isConfirmModalVisible" class="fixed inset-0 bg-black/75 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
|
<div v-if="isConfirmModalVisible"
|
||||||
<div class="bg-white rounded-xl shadow-lg max-w-md w-full p-6 transform transition-all duration-300 scale-95 opacity-0 animate-fadeIn">
|
class="fixed inset-0 bg-black/75 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-xl shadow-lg max-w-md w-full p-6 transform transition-all duration-300 scale-95 opacity-0 animate-fadeIn">
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<div class="flex-shrink-0 w-10 h-10 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
<div class="flex-shrink-0 w-10 h-10 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
||||||
<i class="fas fa-exclamation-triangle text-red-600"></i>
|
<i class="fas fa-exclamation-triangle text-red-600"></i>
|
||||||
@ -154,10 +155,12 @@
|
|||||||
<p class="text-sm text-gray-500" v-html="confirmModalMessage"></p>
|
<p class="text-sm text-gray-500" v-html="confirmModalMessage"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button @click="closeConfirmModal" class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50 transition">
|
<button @click="closeConfirmModal"
|
||||||
|
class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50 transition">
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="handleConfirmAction" class="px-4 py-2 rounded bg-red-500 hover:bg-red-600 text-white transition">
|
<button @click="handleConfirmAction"
|
||||||
|
class="px-4 py-2 rounded bg-red-500 hover:bg-red-600 text-white transition">
|
||||||
{{ confirmText }}
|
{{ confirmText }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -181,7 +184,6 @@ const props = defineProps({
|
|||||||
const items = ref([]);
|
const items = ref([]);
|
||||||
const trays = ref([]);
|
const trays = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref(null);
|
|
||||||
const alert = ref(null);
|
const alert = ref(null);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
|
||||||
@ -264,7 +266,6 @@ const confirmDelete = async () => {
|
|||||||
// Auto hide alert
|
// Auto hide alert
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => { alert.value = null; }, 3000);
|
timer.value = setTimeout(() => { alert.value = null; }, 3000);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal menghapus item:", err.response?.data || err);
|
console.error("Gagal menghapus item:", err.response?.data || err);
|
||||||
alert.value = { error: err.response?.data?.message || "Gagal menghapus item. Silakan coba lagi." };
|
alert.value = { error: err.response?.data?.message || "Gagal menghapus item. Silakan coba lagi." };
|
||||||
@ -275,7 +276,6 @@ const confirmDelete = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const cancelDelete = () => {
|
const cancelDelete = () => {
|
||||||
showDeleteConfirm.value = false;
|
showDeleteConfirm.value = false;
|
||||||
};
|
};
|
||||||
@ -308,7 +308,6 @@ const saveMove = async () => {
|
|||||||
// Auto hide alert
|
// Auto hide alert
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => { alert.value = null; }, 3000);
|
timer.value = setTimeout(() => { alert.value = null; }, 3000);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal memindahkan item:", err.response?.data || err);
|
console.error("Gagal memindahkan item:", err.response?.data || err);
|
||||||
errorMove.value = err.response?.data?.message || "Gagal memindahkan item. Silakan coba lagi.";
|
errorMove.value = err.response?.data?.message || "Gagal memindahkan item. Silakan coba lagi.";
|
||||||
@ -331,7 +330,7 @@ const handleConfirmAction = async () => {
|
|||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fungsi utilitas
|
// Print QR menggunakan browser
|
||||||
const printQR = () => {
|
const printQR = () => {
|
||||||
if (qrCodeUrl.value && selectedItem.value) {
|
if (qrCodeUrl.value && selectedItem.value) {
|
||||||
const printWindow = window.open('', '_blank');
|
const printWindow = window.open('', '_blank');
|
||||||
@ -342,35 +341,80 @@ const printQR = () => {
|
|||||||
<head>
|
<head>
|
||||||
<title>Print QR Code - ${itemCode}</title>
|
<title>Print QR Code - ${itemCode}</title>
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 38mm 25mm;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
width: 38mm;
|
||||||
padding: 20px;
|
height: 25mm;
|
||||||
|
padding: 2mm;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-container {
|
.qr-container {
|
||||||
border: 2px solid #ccc;
|
width: 100%;
|
||||||
padding: 20px;
|
height: 100%;
|
||||||
display: inline-block;
|
display: flex;
|
||||||
margin: 20px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-img {
|
.qr-img {
|
||||||
width: 200px;
|
width: 18mm;
|
||||||
height: 200px;
|
height: 18mm;
|
||||||
|
margin-bottom: 1mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 8pt;
|
||||||
|
margin-bottom: 0.5mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: 6pt;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-weight {
|
||||||
|
font-size: 6pt;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
.item-info {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
||||||
<div class="item-info">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${itemCode}</div>
|
|
||||||
<div>${selectedItem.value.produk?.nama || ''}</div>
|
|
||||||
<div style="color: #666; margin-top: 5px;">${selectedItem.value.produk?.berat || ''}g</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -386,7 +430,6 @@ const printQR = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleImageError = (event) => {
|
const handleImageError = (event) => {
|
||||||
event.target.style.display = 'none';
|
event.target.style.display = 'none';
|
||||||
};
|
};
|
||||||
@ -421,9 +464,17 @@ onMounted(refreshData);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: scale(0.95); }
|
from {
|
||||||
to { opacity: 1; transform: scale(1); }
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animate-fadeIn {
|
.animate-fadeIn {
|
||||||
animation: fadeIn 0.25s ease-out forwards;
|
animation: fadeIn 0.25s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
class="flex-1 px-6 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-lg transition-colors">
|
class="flex-1 px-6 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-lg transition-colors">
|
||||||
Selesai
|
Selesai
|
||||||
</button>
|
</button>
|
||||||
<button @click="printItem"
|
<button @click="printQR"
|
||||||
class="flex-1 px-6 py-2 bg-C hover:bg-B text-D rounded-lg transition-colors">
|
class="flex-1 px-6 py-2 bg-C hover:bg-B text-D rounded-lg transition-colors">
|
||||||
<i class="fas fa-print mr-1"></i>Print
|
<i class="fas fa-print mr-1"></i>Print
|
||||||
</button>
|
</button>
|
||||||
@ -175,45 +175,90 @@ const addNewItem = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fungsi print berdasarkan logika dari brankas list
|
// Fungsi print berdasarkan logika dari brankas list
|
||||||
const printItem = () => {
|
const printQR = () => {
|
||||||
if (qrCodeUrl.value && createdItem.value && props.product) {
|
if (qrCodeUrl.value && createdItem.value && props.product) {
|
||||||
const printWindow = window.open('', '_blank');
|
const printWindow = window.open('', '_blank');
|
||||||
const itemCode = createdItem.value.kode_item || createdItem.value.id;
|
const itemCode = createdItem.value.kode_item;
|
||||||
|
|
||||||
printWindow.document.write(`
|
printWindow.document.write(`
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Print QR Code - ${itemCode}</title>
|
<title>Print QR Code - ${itemCode}</title>
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 38mm 25mm;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
width: 38mm;
|
||||||
padding: 20px;
|
height: 25mm;
|
||||||
|
padding: 2mm;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-container {
|
.qr-container {
|
||||||
border: 2px solid #ccc;
|
width: 100%;
|
||||||
padding: 20px;
|
height: 100%;
|
||||||
display: inline-block;
|
display: flex;
|
||||||
margin: 20px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-img {
|
.qr-img {
|
||||||
width: 200px;
|
width: 18mm;
|
||||||
height: 200px;
|
height: 18mm;
|
||||||
|
margin-bottom: 1mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 8pt;
|
||||||
|
margin-bottom: 0.5mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: 6pt;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-weight {
|
||||||
|
font-size: 6pt;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
.item-info {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
||||||
<div class="item-info">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${itemCode}</div>
|
|
||||||
<div>${props.product.nama}</div>
|
|
||||||
<div style="color: #666; margin-top: 5px;">${props.product.berat}g</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
189
resources/js/components/NiimbotConnector.vue
Normal file
189
resources/js/components/NiimbotConnector.vue
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<!-- Komponen ini digunakan jika menggunakan library @mmote/niimbluelib -->
|
||||||
|
<template>
|
||||||
|
<div v-if="show" class="fixed inset-0 bg-black/75 flex items-center justify-center p-4 z-50 backdrop-blur-sm">
|
||||||
|
<div class="bg-white rounded-xl shadow-lg max-w-md w-full p-6 relative transform transition-all duration-300">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-bold text-D">
|
||||||
|
<i class="fas fa-print mr-2"></i>
|
||||||
|
Koneksi Printer Niimbot
|
||||||
|
</h3>
|
||||||
|
<button @click="$emit('close')" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connection Status -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div v-if="isConnected" class="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-green-900">Terhubung</div>
|
||||||
|
<div class="text-sm text-green-700">{{ connectedPrinterName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="handleDisconnect" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition text-sm">
|
||||||
|
<i class="fas fa-power-off mr-1"></i>
|
||||||
|
Putus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="connectionState === 'connecting'" class="flex items-center gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||||||
|
<span class="text-blue-900">Menghubungkan...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="p-3 bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm mb-2">Status: Tidak terhubung</div>
|
||||||
|
|
||||||
|
<!-- Connection Type Selector -->
|
||||||
|
<div class="flex gap-2 mb-3">
|
||||||
|
<button
|
||||||
|
v-if="featureSupport.webBluetooth"
|
||||||
|
@click="connectionType = 'bluetooth'"
|
||||||
|
:class="[
|
||||||
|
'flex-1 px-3 py-2 rounded border transition text-sm',
|
||||||
|
connectionType === 'bluetooth'
|
||||||
|
? 'bg-D text-white border-D'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]">
|
||||||
|
<i class="fab fa-bluetooth-b mr-1"></i>
|
||||||
|
Bluetooth
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="featureSupport.webSerial"
|
||||||
|
@click="connectionType = 'serial'"
|
||||||
|
:class="[
|
||||||
|
'flex-1 px-3 py-2 rounded border transition text-sm',
|
||||||
|
connectionType === 'serial'
|
||||||
|
? 'bg-D text-white border-D'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]">
|
||||||
|
<i class="fas fa-usb mr-1"></i>
|
||||||
|
USB
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connect Button -->
|
||||||
|
<button
|
||||||
|
@click="handleConnect"
|
||||||
|
:disabled="!canConnect"
|
||||||
|
:class="[
|
||||||
|
'w-full px-4 py-2 rounded transition flex items-center justify-center',
|
||||||
|
canConnect
|
||||||
|
? 'bg-D text-white hover:bg-D/80'
|
||||||
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
|
]">
|
||||||
|
<i class="fas fa-power mr-2"></i>
|
||||||
|
Hubungkan Printer
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="!canConnect" class="text-xs text-red-500 mt-2 text-center">
|
||||||
|
Browser Anda tidak mendukung koneksi printer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Printer Info (when connected) -->
|
||||||
|
<div v-if="isConnected && printerInfo" class="mb-4">
|
||||||
|
<button
|
||||||
|
@click="showDetails = !showDetails"
|
||||||
|
class="w-full flex items-center justify-between p-2 bg-gray-50 rounded hover:bg-gray-100 transition">
|
||||||
|
<span class="text-sm font-medium text-D">Detail Printer</span>
|
||||||
|
<i :class="['fas transition-transform', showDetails ? 'fa-chevron-up' : 'fa-chevron-down']"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="showDetails" class="mt-2 p-3 bg-gray-50 rounded text-sm">
|
||||||
|
<div v-for="(value, key) in printerInfo" :key="key" class="flex justify-between py-1">
|
||||||
|
<span class="text-gray-600">{{ key }}:</span>
|
||||||
|
<span class="font-medium text-D">{{ value || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Display -->
|
||||||
|
<div v-if="error" class="mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Text -->
|
||||||
|
<div class="text-xs text-gray-500 p-3 bg-gray-50 rounded">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
<strong>Tips:</strong> Pastikan printer Niimbot sudah dihidupkan dan mode pairing aktif (untuk Bluetooth) atau terhubung via USB.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { useNiimbotPrinter } from '../composables/useNiimbotPrinter';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'connected', 'disconnected']);
|
||||||
|
|
||||||
|
// Use Niimbot composable
|
||||||
|
const {
|
||||||
|
connectionState,
|
||||||
|
connectedPrinterName,
|
||||||
|
printerInfo,
|
||||||
|
printerMeta,
|
||||||
|
isConnected,
|
||||||
|
isDisconnected,
|
||||||
|
featureSupport,
|
||||||
|
connectionType,
|
||||||
|
initClient,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
} = useNiimbotPrinter();
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const error = ref('');
|
||||||
|
const showDetails = ref(false);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const canConnect = computed(() => {
|
||||||
|
return featureSupport.value.webBluetooth || featureSupport.value.webSerial;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleConnect = async () => {
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await connect();
|
||||||
|
emit('connected');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Connection error:', err);
|
||||||
|
error.value = err.message || 'Gagal terhubung ke printer. Pastikan printer sudah dinyalakan dan dalam mode pairing.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
disconnect();
|
||||||
|
emit('disconnected');
|
||||||
|
error.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch connection state changes
|
||||||
|
watch(isConnected, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
error.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -186,35 +186,80 @@ const printQR = () => {
|
|||||||
<head>
|
<head>
|
||||||
<title>Print QR Code - ${itemCode}</title>
|
<title>Print QR Code - ${itemCode}</title>
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 38mm 25mm;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
width: 38mm;
|
||||||
padding: 20px;
|
height: 25mm;
|
||||||
|
padding: 2mm;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-container {
|
.qr-container {
|
||||||
border: 2px solid #ccc;
|
width: 100%;
|
||||||
padding: 20px;
|
height: 100%;
|
||||||
display: inline-block;
|
display: flex;
|
||||||
margin: 20px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-img {
|
.qr-img {
|
||||||
width: 200px;
|
width: 18mm;
|
||||||
height: 200px;
|
height: 18mm;
|
||||||
|
margin-bottom: 1mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 8pt;
|
||||||
|
margin-bottom: 0.5mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: 6pt;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 34mm;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-weight {
|
||||||
|
font-size: 6pt;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
.item-info {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
||||||
<div class="item-info">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${itemCode}</div>
|
|
||||||
<div>${selectedItem.value.produk?.nama || ''}</div>
|
|
||||||
<div style="color: #666; margin-top: 5px;">${selectedItem.value.produk?.berat || ''}g</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
298
resources/js/composables/useNiimbotPrinter.js
Normal file
298
resources/js/composables/useNiimbotPrinter.js
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* Menggunakan library@mmote/niimbluelib
|
||||||
|
*/
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import {
|
||||||
|
NiimbotBluetoothClient,
|
||||||
|
NiimbotSerialClient,
|
||||||
|
Utils,
|
||||||
|
ImageEncoder,
|
||||||
|
LabelType
|
||||||
|
} from '@mmote/niimbluelib';
|
||||||
|
|
||||||
|
export function useNiimbotPrinter() {
|
||||||
|
// State
|
||||||
|
const printerClient = ref(null);
|
||||||
|
const connectionState = ref('disconnected'); // 'disconnected' | 'connecting' | 'connected'
|
||||||
|
const connectedPrinterName = ref('');
|
||||||
|
const printerInfo = ref(null);
|
||||||
|
const printerMeta = ref(null);
|
||||||
|
const heartbeatData = ref(null);
|
||||||
|
const connectionType = ref('bluetooth'); // 'bluetooth' | 'serial'
|
||||||
|
const printProgress = ref(0);
|
||||||
|
const isPrinting = ref(false);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const isConnected = computed(() => connectionState.value === 'connected');
|
||||||
|
const isDisconnected = computed(() => connectionState.value === 'disconnected');
|
||||||
|
const featureSupport = computed(() => Utils.getAvailableTransports());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi printer client
|
||||||
|
*/
|
||||||
|
const initClient = (type = 'bluetooth') => {
|
||||||
|
connectionType.value = type;
|
||||||
|
|
||||||
|
// Disconnect existing client
|
||||||
|
if (printerClient.value) {
|
||||||
|
printerClient.value.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new client based on type
|
||||||
|
if (type === 'bluetooth') {
|
||||||
|
printerClient.value = new NiimbotBluetoothClient();
|
||||||
|
} else if (type === 'serial') {
|
||||||
|
printerClient.value = new NiimbotSerialClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
printerClient.value.on('connect', (e) => {
|
||||||
|
console.log('Printer connected:', e);
|
||||||
|
connectionState.value = 'connected';
|
||||||
|
connectedPrinterName.value = e.info.deviceName || 'Niimbot Printer';
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('connect', (e) => {
|
||||||
|
console.log('Printer connected (Serial):', e);
|
||||||
|
console.log('Device details:', e.info);
|
||||||
|
connectionState.value = 'connected';
|
||||||
|
connectedPrinterName.value = e.info.deviceName || 'Niimbot Printer';
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('packetreceived', (e) => {
|
||||||
|
console.log('<< Packet received (Serial):', Utils.bufToHex(e.packet.toBytes()), e);
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('disconnect', () => {
|
||||||
|
console.log('Printer disconnected');
|
||||||
|
connectionState.value = 'disconnected';
|
||||||
|
connectedPrinterName.value = '';
|
||||||
|
printerInfo.value = null;
|
||||||
|
printerMeta.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('printerinfofetched', (e) => {
|
||||||
|
console.log('Printer info fetched:', e.info);
|
||||||
|
printerInfo.value = e.info;
|
||||||
|
printerMeta.value = printerClient.value.getModelMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('heartbeat', (e) => {
|
||||||
|
heartbeatData.value = e.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
printerClient.value.on('printprogress', (e) => {
|
||||||
|
printProgress.value = Math.floor(
|
||||||
|
(e.page / e.totalPages) * ((e.pagePrintProgress + e.pageFeedProgress) / 2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log packets for debugging
|
||||||
|
printerClient.value.on('packetsent', (e) => {
|
||||||
|
console.log('>> Packet sent:', Utils.bufToHex(e.packet.toBytes()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// printerClient.value.on('packetreceived', (e) => {
|
||||||
|
// console.log('<< Packet received:', Utils.bufToHex(e.packet.toBytes()));
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to printer
|
||||||
|
*/
|
||||||
|
// const connect = async () => {
|
||||||
|
// if (!printerClient.value) {
|
||||||
|
// initClient(connectionType.value);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// connectionState.value = 'connecting';
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await printerClient.value.connect();
|
||||||
|
// // Connection state will be updated by 'connect' event
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Failed to connect to printer:', error);
|
||||||
|
// connectionState.value = 'disconnected';
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const connect = async () => {
|
||||||
|
if (!printerClient.value) {
|
||||||
|
initClient(connectionType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState.value = 'connecting';
|
||||||
|
try {
|
||||||
|
const port = await printerClient.value.connect();
|
||||||
|
console.log('Selected serial port:', port);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to printer (Serial):', error);
|
||||||
|
connectionState.value = 'disconnected';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from printer
|
||||||
|
*/
|
||||||
|
const disconnect = () => {
|
||||||
|
if (printerClient.value) {
|
||||||
|
printerClient.value.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print QR Code image
|
||||||
|
* @param {string} imageDataUrl - Data URL of QR code image
|
||||||
|
* @param {object} options - Print options
|
||||||
|
*/
|
||||||
|
const printQRCode = async (imageDataUrl, options = {}) => {
|
||||||
|
if (!printerClient.value || !isConnected.value) {
|
||||||
|
throw new Error('Printer not connected. Please connect to printer first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
density = 3,
|
||||||
|
quantity = 1,
|
||||||
|
labelType = LabelType.WithGaps,
|
||||||
|
printTaskName = 'D110'
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
isPrinting.value = true;
|
||||||
|
printProgress.value = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop heartbeat during print
|
||||||
|
printerClient.value.stopHeartbeat();
|
||||||
|
|
||||||
|
// Create print task
|
||||||
|
const printTask = printerClient.value.abstraction.newPrintTask(printTaskName, {
|
||||||
|
totalPages: quantity,
|
||||||
|
density,
|
||||||
|
labelType,
|
||||||
|
statusPollIntervalMs: 100,
|
||||||
|
statusTimeoutMs: 8000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load image into canvas
|
||||||
|
const canvas = await loadImageToCanvas(imageDataUrl);
|
||||||
|
|
||||||
|
// Encode canvas to printer format
|
||||||
|
const encoded = ImageEncoder.encodeCanvas(canvas, 'top'); // 'top' = print direction
|
||||||
|
|
||||||
|
// Initialize print
|
||||||
|
await printTask.printInit();
|
||||||
|
|
||||||
|
// Send print data
|
||||||
|
await printTask.printPage(encoded, quantity);
|
||||||
|
|
||||||
|
// Wait for print to finish
|
||||||
|
await printTask.waitForFinished();
|
||||||
|
|
||||||
|
// End print
|
||||||
|
await printTask.printEnd();
|
||||||
|
|
||||||
|
printProgress.value = 100;
|
||||||
|
|
||||||
|
// Restart heartbeat
|
||||||
|
printerClient.value.startHeartbeat();
|
||||||
|
|
||||||
|
return { success: true, message: 'QR Code printed successfully' };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Print failed:', error);
|
||||||
|
|
||||||
|
// Try to end print gracefully
|
||||||
|
try {
|
||||||
|
await printerClient.value.abstraction.printEnd();
|
||||||
|
printerClient.value.startHeartbeat();
|
||||||
|
} catch (endError) {
|
||||||
|
console.error('Failed to end print:', endError);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
isPrinting.value = false;
|
||||||
|
printProgress.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load image data URL into canvas
|
||||||
|
* @param {string} dataUrl - Image data URL
|
||||||
|
* @returns {HTMLCanvasElement}
|
||||||
|
*/
|
||||||
|
const loadImageToCanvas = (dataUrl) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
resolve(canvas);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error('Failed to load image'));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = dataUrl;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply threshold to canvas for better print quality
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @param {number} threshold - 0-255
|
||||||
|
*/
|
||||||
|
const applyThreshold = (canvas, threshold = 140) => {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
||||||
|
const val = avg < threshold ? 0 : 255;
|
||||||
|
data[i] = val; // red
|
||||||
|
data[i + 1] = val; // green
|
||||||
|
data[i + 2] = val; // blue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
printerClient,
|
||||||
|
connectionState,
|
||||||
|
connectedPrinterName,
|
||||||
|
printerInfo,
|
||||||
|
printerMeta,
|
||||||
|
heartbeatData,
|
||||||
|
connectionType,
|
||||||
|
printProgress,
|
||||||
|
isPrinting,
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
isConnected,
|
||||||
|
isDisconnected,
|
||||||
|
featureSupport,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
initClient,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
printQRCode,
|
||||||
|
loadImageToCanvas,
|
||||||
|
applyThreshold,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -54,24 +54,23 @@
|
|||||||
|
|
||||||
<div class="mb-3 flex flex-row w-full gap-3">
|
<div class="mb-3 flex flex-row w-full gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1"
|
<label class="block text-D mb-1">Harga per Gram</label>
|
||||||
>Harga per Gram</label
|
|
||||||
>
|
|
||||||
<InputField
|
<InputField
|
||||||
v-model="form.harga_per_gram"
|
v-model="hargaPerGramFormatted"
|
||||||
type="number"
|
type="text"
|
||||||
step="0.01"
|
|
||||||
placeholder="Masukkan harga per gram"
|
placeholder="Masukkan harga per gram"
|
||||||
@input="calculateHargaJual"
|
@input="formatHargaPerGramInput"
|
||||||
|
@keypress="onlyNumbers"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Harga Jual</label>
|
<label class="block text-D mb-1">Harga Jual</label>
|
||||||
<InputField
|
<InputField
|
||||||
v-model="form.harga_jual"
|
v-model="hargaJualFormatted"
|
||||||
type="number"
|
type="text"
|
||||||
step="0.01"
|
|
||||||
placeholder="Masukkan harga jual"
|
placeholder="Masukkan harga jual"
|
||||||
|
@input="formatHargaJualInput"
|
||||||
|
@keypress="onlyNumbers"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -225,7 +224,6 @@ import mainLayout from "../layouts/mainLayout.vue";
|
|||||||
import InputField from "../components/InputField.vue";
|
import InputField from "../components/InputField.vue";
|
||||||
import InputSelect from "../components/InputSelect.vue";
|
import InputSelect from "../components/InputSelect.vue";
|
||||||
import CreateItemModal from "../components/CreateItemModal.vue";
|
import CreateItemModal from "../components/CreateItemModal.vue";
|
||||||
import { errorMessages } from "@vue/compiler-core";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -237,8 +235,8 @@ const form = ref({
|
|||||||
id_kategori: null,
|
id_kategori: null,
|
||||||
berat: 0,
|
berat: 0,
|
||||||
kadar: 0,
|
kadar: 0,
|
||||||
harga_per_gram: 0,
|
harga_per_gram: null,
|
||||||
harga_jual: 0,
|
harga_jual: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = ref([]);
|
const category = ref([]);
|
||||||
@ -252,6 +250,62 @@ const fileInput = ref(null);
|
|||||||
const openItemModal = ref(false);
|
const openItemModal = ref(false);
|
||||||
const editedProduct = ref(null);
|
const editedProduct = ref(null);
|
||||||
|
|
||||||
|
// Formatted values for harga_per_gram and harga_jual
|
||||||
|
const hargaPerGramFormatted = ref("");
|
||||||
|
const hargaJualFormatted = ref("");
|
||||||
|
|
||||||
|
// Format angka dengan pemisah ribuan
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (!num) return "";
|
||||||
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Menghapus format dan mengambil angka asli
|
||||||
|
const unformatNumber = (str) => {
|
||||||
|
if (!str) return null;
|
||||||
|
const cleaned = str.replace(/\./g, "");
|
||||||
|
const number = parseFloat(cleaned);
|
||||||
|
return isNaN(number) ? null : number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler untuk format input harga per gram
|
||||||
|
const formatHargaPerGramInput = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const cleanValue = value.replace(/\D/g, "");
|
||||||
|
if (cleanValue) {
|
||||||
|
const formatted = formatNumber(cleanValue);
|
||||||
|
hargaPerGramFormatted.value = formatted;
|
||||||
|
form.value.harga_per_gram = parseFloat(cleanValue);
|
||||||
|
calculateHargaJual();
|
||||||
|
} else {
|
||||||
|
hargaPerGramFormatted.value = "";
|
||||||
|
form.value.harga_per_gram = null;
|
||||||
|
calculateHargaJual();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler untuk format input harga jual
|
||||||
|
const formatHargaJualInput = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const cleanValue = value.replace(/\D/g, "");
|
||||||
|
if (cleanValue) {
|
||||||
|
const formatted = formatNumber(cleanValue);
|
||||||
|
hargaJualFormatted.value = formatted;
|
||||||
|
form.value.harga_jual = parseFloat(cleanValue);
|
||||||
|
} else {
|
||||||
|
hargaJualFormatted.value = "";
|
||||||
|
form.value.harga_jual = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hanya izinkan angka saat mengetik
|
||||||
|
const onlyNumbers = (event) => {
|
||||||
|
const char = String.fromCharCode(event.which);
|
||||||
|
if (!/[0-9]/.test(char)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
return (
|
||||||
form.value.nama &&
|
form.value.nama &&
|
||||||
@ -267,7 +321,12 @@ const calculateHargaJual = () => {
|
|||||||
const berat = parseFloat(form.value.berat) || 0;
|
const berat = parseFloat(form.value.berat) || 0;
|
||||||
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
||||||
if (berat > 0 && hargaPerGram > 0) {
|
if (berat > 0 && hargaPerGram > 0) {
|
||||||
form.value.harga_jual = berat * hargaPerGram;
|
const hargaJual = berat * hargaPerGram;
|
||||||
|
form.value.harga_jual = hargaJual;
|
||||||
|
hargaJualFormatted.value = formatNumber(hargaJual.toFixed(0));
|
||||||
|
} else {
|
||||||
|
form.value.harga_jual = null;
|
||||||
|
hargaJualFormatted.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -287,8 +346,6 @@ const loadProduk = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const produk = response.data;
|
const produk = response.data;
|
||||||
// console.log(produk);
|
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
nama: produk.nama,
|
nama: produk.nama,
|
||||||
id_kategori: produk.id_kategori,
|
id_kategori: produk.id_kategori,
|
||||||
@ -297,6 +354,9 @@ const loadProduk = async () => {
|
|||||||
harga_per_gram: produk.harga_per_gram,
|
harga_per_gram: produk.harga_per_gram,
|
||||||
harga_jual: produk.harga_jual,
|
harga_jual: produk.harga_jual,
|
||||||
};
|
};
|
||||||
|
// Set formatted values after loading
|
||||||
|
hargaPerGramFormatted.value = formatNumber(produk.harga_per_gram?.toString() || "");
|
||||||
|
hargaJualFormatted.value = formatNumber(produk.harga_jual?.toString() || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFoto = async () => {
|
const loadFoto = async () => {
|
||||||
@ -307,10 +367,8 @@ const loadFoto = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
uploadedImages.value = response.data;
|
uploadedImages.value = response.data;
|
||||||
// console.log(uploadedImages.value);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
uploadError.value = "Gagal memuat foto";
|
uploadError.value = "Gagal memuat foto";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -417,7 +475,7 @@ const submitForm = async () => {
|
|||||||
);
|
);
|
||||||
router.push("/produk?message=Produk berhasil diperbarui");
|
router.push("/produk?message=Produk berhasil diperbarui");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessages.value = err.response?.data?.message || "Gagal menyimpan produk";
|
uploadError.value = err.response?.data?.message || "Gagal menyimpan produk";
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|||||||
@ -35,13 +35,23 @@
|
|||||||
<div class="mb-3 flex flex-row w-full gap-3">
|
<div class="mb-3 flex flex-row w-full gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Harga per Gram</label>
|
<label class="block text-D mb-1">Harga per Gram</label>
|
||||||
<InputField v-model="form.harga_per_gram" type="number" step="0.01"
|
<InputField
|
||||||
placeholder="Masukkan harga per gram" @input="calculateHargaJual" />
|
v-model="hargaPerGramFormatted"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan harga per gram"
|
||||||
|
@input="formatHargaPerGramInput"
|
||||||
|
@keypress="onlyNumbers"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="block text-D mb-1">Harga Jual</label>
|
<label class="block text-D mb-1">Harga Jual</label>
|
||||||
<InputField v-model="form.harga_jual" type="number" step="0.01"
|
<InputField
|
||||||
placeholder="Masukkan harga jual" />
|
v-model="hargaJualFormatted"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan harga jual"
|
||||||
|
@input="formatHargaJualInput"
|
||||||
|
@keypress="onlyNumbers"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -174,7 +184,12 @@ import CreateItemModal from "../components/CreateItemModal.vue";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
nama: '', id_kategori: null, berat: null, kadar: null, harga_per_gram: null, harga_jual: null,
|
nama: '',
|
||||||
|
id_kategori: null,
|
||||||
|
berat: null,
|
||||||
|
kadar: null,
|
||||||
|
harga_per_gram: null,
|
||||||
|
harga_jual: null,
|
||||||
});
|
});
|
||||||
const category = ref([]);
|
const category = ref([]);
|
||||||
const showUploadMenu = ref(false);
|
const showUploadMenu = ref(false);
|
||||||
@ -194,17 +209,84 @@ const video = ref(null);
|
|||||||
const canvas = ref(null);
|
const canvas = ref(null);
|
||||||
let stream = null;
|
let stream = null;
|
||||||
|
|
||||||
|
// Formatted values for harga_per_gram and harga_jual
|
||||||
|
const hargaPerGramFormatted = ref("");
|
||||||
|
const hargaJualFormatted = ref("");
|
||||||
|
|
||||||
|
// Format angka dengan pemisah ribuan
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (!num) return "";
|
||||||
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Menghapus format dan mengambil angka asli
|
||||||
|
const unformatNumber = (str) => {
|
||||||
|
if (!str) return null;
|
||||||
|
const cleaned = str.replace(/\./g, "");
|
||||||
|
const number = parseFloat(cleaned);
|
||||||
|
return isNaN(number) ? null : number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler untuk format input harga per gram
|
||||||
|
const formatHargaPerGramInput = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const cleanValue = value.replace(/\D/g, "");
|
||||||
|
if (cleanValue) {
|
||||||
|
const formatted = formatNumber(cleanValue);
|
||||||
|
hargaPerGramFormatted.value = formatted;
|
||||||
|
form.value.harga_per_gram = parseFloat(cleanValue);
|
||||||
|
calculateHargaJual();
|
||||||
|
} else {
|
||||||
|
hargaPerGramFormatted.value = "";
|
||||||
|
form.value.harga_per_gram = null;
|
||||||
|
calculateHargaJual();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler untuk format input harga jual
|
||||||
|
const formatHargaJualInput = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const cleanValue = value.replace(/\D/g, "");
|
||||||
|
if (cleanValue) {
|
||||||
|
const formatted = formatNumber(cleanValue);
|
||||||
|
hargaJualFormatted.value = formatted;
|
||||||
|
form.value.harga_jual = parseFloat(cleanValue);
|
||||||
|
} else {
|
||||||
|
hargaJualFormatted.value = "";
|
||||||
|
form.value.harga_jual = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hanya izinkan angka saat mengetik
|
||||||
|
const onlyNumbers = (event) => {
|
||||||
|
const char = String.fromCharCode(event.which);
|
||||||
|
if (!/[0-9]/.test(char)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return form.value.nama && form.value.id_kategori && form.value.berat > 0 &&
|
return (
|
||||||
form.value.kadar > 0 && form.value.harga_per_gram > 0 && form.value.harga_jual > 0 &&
|
form.value.nama &&
|
||||||
uploadedImages.value.length > 0;
|
form.value.id_kategori &&
|
||||||
|
form.value.berat > 0 &&
|
||||||
|
form.value.kadar > 0 &&
|
||||||
|
form.value.harga_per_gram > 0 &&
|
||||||
|
form.value.harga_jual > 0 &&
|
||||||
|
uploadedImages.value.length > 0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const calculateHargaJual = () => {
|
const calculateHargaJual = () => {
|
||||||
const berat = parseFloat(form.value.berat) || 0;
|
const berat = parseFloat(form.value.berat) || 0;
|
||||||
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0;
|
||||||
if (berat > 0 && hargaPerGram > 0) {
|
if (berat > 0 && hargaPerGram > 0) {
|
||||||
form.value.harga_jual = berat * hargaPerGram;
|
const hargaJual = berat * hargaPerGram;
|
||||||
|
form.value.harga_jual = hargaJual;
|
||||||
|
hargaJualFormatted.value = formatNumber(hargaJual.toFixed(0));
|
||||||
|
} else {
|
||||||
|
form.value.harga_jual = null;
|
||||||
|
hargaJualFormatted.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -214,7 +296,7 @@ const loadKategori = async () => {
|
|||||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||||
});
|
});
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
category.value = response.data.map(cat => ({ value: cat.id, label: cat.nama }));
|
category.value = response.data.map((cat) => ({ value: cat.id, label: cat.nama }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading categories:', error);
|
console.error('Error loading categories:', error);
|
||||||
@ -263,11 +345,17 @@ const uploadFiles = async (files) => {
|
|||||||
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
|
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const validFiles = files.filter(file => {
|
const validFiles = files.filter((file) => {
|
||||||
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
|
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
|
||||||
const isValidSize = file.size <= 2 * 1024 * 1024;
|
const isValidSize = file.size <= 2 * 1024 * 1024;
|
||||||
if (!isValidType) { uploadError.value = 'Format file harus JPG, JPEG, atau PNG'; return false; }
|
if (!isValidType) {
|
||||||
if (!isValidSize) { uploadError.value = 'Ukuran file maksimal 2MB'; return false; }
|
uploadError.value = 'Format file harus JPG, JPEG, atau PNG';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isValidSize) {
|
||||||
|
uploadError.value = 'Ukuran file maksimal 2MB';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (validFiles.length === 0) return;
|
if (validFiles.length === 0) return;
|
||||||
@ -277,7 +365,10 @@ const uploadFiles = async (files) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('foto', file);
|
formData.append('foto', file);
|
||||||
const response = await axios.post('/api/foto', formData, {
|
const response = await axios.post('/api/foto', formData, {
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, 'Content-Type': 'multipart/form-data' },
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
uploadedImages.value.push(response.data);
|
uploadedImages.value.push(response.data);
|
||||||
}
|
}
|
||||||
@ -318,7 +409,7 @@ const openCameraModal = async () => {
|
|||||||
const closeCamera = () => {
|
const closeCamera = () => {
|
||||||
showCamera.value = false;
|
showCamera.value = false;
|
||||||
if (stream) {
|
if (stream) {
|
||||||
stream.getTracks().forEach(track => track.stop());
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
stream = null;
|
stream = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -346,7 +437,16 @@ const submitForm = async (addItem) => {
|
|||||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||||
});
|
});
|
||||||
const createdProductData = response.data.data;
|
const createdProductData = response.data.data;
|
||||||
form.value = { nama: '', id_kategori: '', berat: 0, kadar: 0, harga_per_gram: 0, harga_jual: 0 };
|
form.value = {
|
||||||
|
nama: '',
|
||||||
|
id_kategori: '',
|
||||||
|
berat: 0,
|
||||||
|
kadar: 0,
|
||||||
|
harga_per_gram: 0,
|
||||||
|
harga_jual: 0,
|
||||||
|
};
|
||||||
|
hargaPerGramFormatted.value = "";
|
||||||
|
hargaJualFormatted.value = "";
|
||||||
uploadedImages.value = [];
|
uploadedImages.value = [];
|
||||||
uploadError.value = '';
|
uploadError.value = '';
|
||||||
showUploadMenu.value = false;
|
showUploadMenu.value = false;
|
||||||
@ -368,9 +468,24 @@ const submitForm = async (addItem) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = () => { router.push('/produk'); };
|
const back = () => {
|
||||||
const openCreateItemModal = (product) => { createdProduct.value = product; openItemModal.value = true; };
|
router.push('/produk');
|
||||||
const closeItemModal = () => { openItemModal.value = false; createdProduct.value = null; };
|
};
|
||||||
|
|
||||||
onMounted(() => { loadFoto(); loadKategori(); });
|
const openCreateItemModal = (product) => {
|
||||||
|
createdProduct.value = product;
|
||||||
|
openItemModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeItemModal = () => {
|
||||||
|
openItemModal.value = false;
|
||||||
|
createdProduct.value = null;
|
||||||
|
router.push('/produk');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadFoto();
|
||||||
|
loadKategori();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user