Last active
May 30, 2025 01:02
-
-
Save esafwan/7df2f80935ba27326e253c4ee8bfb34b to your computer and use it in GitHub Desktop.
Qz Tray React (Next)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client" | |
| import { useState, useEffect } from "react" | |
| import { Button } from "@/components/ui/button" | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" | |
| import { Printer, Wifi, WifiOff, RefreshCw, AlertTriangle } from "lucide-react" | |
| declare global { | |
| interface Window { | |
| qz: any | |
| } | |
| } | |
| export default function ReceiptPrintPage() { | |
| const [isConnected, setIsConnected] = useState(false) | |
| const [printers, setPrinters] = useState<string[]>([]) | |
| const [selectedPrinter, setSelectedPrinter] = useState<string>("BIXOLON SRP-350III") | |
| const [isLoading, setIsLoading] = useState(false) | |
| const [debugInfo, setDebugInfo] = useState<string>("") | |
| const [hasBixolonPrinter, setHasBixolonPrinter] = useState<boolean | null>(null) | |
| const [connectionError, setConnectionError] = useState<string | null>(null) | |
| const [isQzLoaded, setIsQzLoaded] = useState(false) | |
| // Fixed configuration for BIXOLON SRP-350III thermal printer | |
| const printConfig = { | |
| paperWidth: 80, | |
| paperHeight: 300, | |
| units: "mm", | |
| marginTop: 0, | |
| marginRight: 0, | |
| marginBottom: 0, | |
| marginLeft: 0, | |
| copies: 1, | |
| density: 1, | |
| rasterize: false, | |
| } | |
| const cutType = "full" // Always use full cut | |
| const receiptData = `Talal Online Grocery | |
| Order No: 100237 | |
| Customer: +971 50 123 4567 | |
| -------------------------------------------------- | |
| Item Qty Price Total | |
| -------------------------------------------------- | |
| Banana (1kg) 1 5.00 5.00 | |
| Milk 1L 2 6.50 13.00 | |
| Bread Pack 1 4.25 4.25 | |
| Eggs (12 pack) 1 10.00 10.00 | |
| Rice 5kg 1 25.00 25.00 | |
| -------------------------------------------------- | |
| TOTAL AED 57.25 | |
| -------------------------------------------------- | |
| Thank you for shopping with Talal Online! | |
| ` | |
| useEffect(() => { | |
| const loadQzTray = async () => { | |
| // Try multiple CDN sources for QZ Tray | |
| const qzSources = [ | |
| "https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.min.js", | |
| "https://cdnjs.cloudflare.com/ajax/libs/qz-tray/2.1.0/qz-tray.js", | |
| "https://unpkg.com/qz-tray@2.2.4/qz-tray.js", | |
| ] | |
| let scriptLoaded = false | |
| let lastError = null | |
| for (const src of qzSources) { | |
| if (scriptLoaded) break | |
| try { | |
| console.log(`Attempting to load QZ Tray from: ${src}`) | |
| await new Promise((resolve, reject) => { | |
| // Remove any existing QZ script | |
| const existingScript = document.querySelector('script[src*="qz-tray"]') | |
| if (existingScript) { | |
| existingScript.remove() | |
| } | |
| const script = document.createElement("script") | |
| script.src = src | |
| script.async = true | |
| script.crossOrigin = "anonymous" | |
| script.onload = () => { | |
| console.log(`QZ Tray script loaded successfully from: ${src}`) | |
| setIsQzLoaded(true) | |
| scriptLoaded = true | |
| resolve(true) | |
| } | |
| script.onerror = (error) => { | |
| console.error(`Failed to load QZ Tray from ${src}:`, error) | |
| document.head.removeChild(script) | |
| reject(new Error(`Failed to load from ${src}`)) | |
| } | |
| document.head.appendChild(script) | |
| // Timeout after 10 seconds | |
| setTimeout(() => { | |
| if (!scriptLoaded) { | |
| console.error(`Timeout loading QZ Tray from ${src}`) | |
| if (document.head.contains(script)) { | |
| document.head.removeChild(script) | |
| } | |
| reject(new Error(`Timeout loading from ${src}`)) | |
| } | |
| }, 10000) | |
| }) | |
| } catch (error) { | |
| lastError = error | |
| console.error(`Error loading QZ Tray from ${src}:`, error) | |
| continue | |
| } | |
| } | |
| if (!scriptLoaded) { | |
| console.error("Failed to load QZ Tray from all sources:", lastError) | |
| setConnectionError( | |
| `Failed to load QZ Tray library from all CDN sources. Please check your internet connection or try downloading QZ Tray locally. Last error: ${lastError?.message || "Unknown error"}`, | |
| ) | |
| return | |
| } | |
| // Wait a bit for the script to initialize | |
| setTimeout(() => { | |
| if (window.qz) { | |
| console.log("QZ Tray object found, attempting connection...") | |
| connectToQZ() | |
| } else { | |
| console.error("QZ Tray script loaded but window.qz not found") | |
| setConnectionError("QZ Tray script loaded but library not initialized properly") | |
| } | |
| }, 1000) | |
| } | |
| loadQzTray() | |
| return () => { | |
| // Cleanup | |
| const scripts = document.querySelectorAll('script[src*="qz-tray"]') | |
| scripts.forEach((script) => { | |
| if (document.head.contains(script)) { | |
| document.head.removeChild(script) | |
| } | |
| }) | |
| } | |
| }, []) | |
| const connectToQZ = async () => { | |
| if (!window.qz) { | |
| console.error("QZ Tray library not found. Script may not have loaded properly.") | |
| setConnectionError("QZ Tray library not found. Script may not have loaded properly.") | |
| return | |
| } | |
| try { | |
| console.log("Attempting to connect to QZ Tray...") | |
| // Check if QZ is already active | |
| const isActive = await window.qz.websocket.isActive() | |
| console.log("QZ Tray active status:", isActive) | |
| if (isActive) { | |
| console.log("QZ Tray is already connected") | |
| setIsConnected(true) | |
| fetchPrinters() | |
| return | |
| } | |
| await window.qz.websocket | |
| .connect({ | |
| retries: 3, | |
| delay: 1000, | |
| }) | |
| .catch((err: any) => { | |
| throw err | |
| }) | |
| console.log("Successfully connected to QZ Tray") | |
| setIsConnected(true) | |
| setConnectionError(null) | |
| fetchPrinters() | |
| } catch (error: any) { | |
| console.error("Failed to connect to QZ Tray:", error) | |
| // Detailed error logging | |
| if (error.message) console.error("Error message:", error.message) | |
| if (error.stack) console.error("Error stack:", error.stack) | |
| let errorMessage = "Failed to connect to QZ Tray. " | |
| // Provide more specific error messages | |
| if (error.message?.includes("SecurityError") || error.message?.includes("secure context")) { | |
| errorMessage += "QZ Tray requires HTTPS in production. Try using localhost or HTTPS." | |
| } else if (error.message?.includes("not found") || error.message?.includes("undefined")) { | |
| errorMessage += "QZ Tray application may not be running or is not responding." | |
| } else if (error.message?.includes("certificate")) { | |
| errorMessage += "Certificate issue. Check QZ Tray certificates." | |
| } else { | |
| errorMessage += error.message || "Unknown error." | |
| } | |
| setConnectionError(errorMessage) | |
| setIsConnected(false) | |
| } | |
| } | |
| const fetchPrinters = async () => { | |
| if (!window.qz || !isConnected) return | |
| try { | |
| console.log("Fetching available printers...") | |
| const availablePrinters = await window.qz.printers.find() | |
| console.log("Available printers:", availablePrinters) | |
| setPrinters(availablePrinters) | |
| // Debug info | |
| const qzVersion = window.qz.version || "Unknown" | |
| console.log("QZ Tray version:", qzVersion) | |
| const debugText = `QZ Tray Version: ${qzVersion} | |
| Total Printers Found: ${availablePrinters.length} | |
| Printer List: | |
| ${availablePrinters.map((p: string, i: number) => `${i + 1}. "${p}"`).join("\n")} | |
| Connection Info: | |
| - Protocol: ${window.location.protocol} | |
| - Hostname: ${window.location.hostname} | |
| - QZ Loaded: ${isQzLoaded ? "Yes" : "No"} | |
| - QZ Connected: ${isConnected ? "Yes" : "No"} | |
| ` | |
| setDebugInfo(debugText) | |
| // Check for BIXOLON printer | |
| const bixolonExists = availablePrinters.some( | |
| (printer: string) => printer.toLowerCase().includes("bixolon") && printer.toLowerCase().includes("srp-350iii"), | |
| ) | |
| setHasBixolonPrinter(bixolonExists) | |
| // Always try to use BIXOLON SRP-350III if available | |
| const bixolonPrinter = availablePrinters.find( | |
| (printer: string) => printer.toLowerCase().includes("bixolon") && printer.toLowerCase().includes("srp-350iii"), | |
| ) | |
| if (bixolonPrinter) { | |
| setSelectedPrinter(bixolonPrinter) | |
| } | |
| } catch (error: any) { | |
| console.error("Failed to fetch printers:", error) | |
| setDebugInfo(`Printer Fetch Error: ${error.message || "Unknown error"}`) | |
| } | |
| } | |
| const printReceipt = async () => { | |
| if (!window.qz || !isConnected || !selectedPrinter) { | |
| alert("QZ Tray not connected or no printer selected") | |
| return | |
| } | |
| setIsLoading(true) | |
| try { | |
| console.log("Creating print config for printer:", selectedPrinter) | |
| console.log("Print configuration:", printConfig) | |
| const config = window.qz.configs.create(selectedPrinter, { | |
| size: { | |
| width: printConfig.paperWidth, | |
| height: printConfig.paperHeight, | |
| units: printConfig.units, | |
| }, | |
| margins: { | |
| top: printConfig.marginTop, | |
| right: printConfig.marginRight, | |
| bottom: printConfig.marginBottom, | |
| left: printConfig.marginLeft, | |
| }, | |
| copies: printConfig.copies, | |
| density: printConfig.density, | |
| rasterize: printConfig.rasterize, | |
| }) | |
| // Create data array with receipt content | |
| const data = [ | |
| { | |
| type: "raw", | |
| format: "plain", | |
| data: receiptData, | |
| }, | |
| ] | |
| // Add full cut command | |
| const cutCommand = "1D5601" // Full cut: GS V 1 | |
| console.log(`Adding full cut command: ${cutCommand}`) | |
| data.push({ | |
| type: "raw", | |
| format: "hex", | |
| data: cutCommand, | |
| }) | |
| console.log("Sending print job to printer with data:", data) | |
| await window.qz.print(config, data) | |
| console.log("Receipt printed successfully!") | |
| alert("Receipt printed successfully!") | |
| } catch (error: any) { | |
| console.error("Print failed:", error) | |
| console.error("Error details:", error.message || "Unknown error") | |
| alert("Print failed: " + (error.message || "Unknown error")) | |
| } finally { | |
| setIsLoading(false) | |
| } | |
| } | |
| const refreshPrinters = async () => { | |
| if (!window.qz) { | |
| console.error("QZ Tray library not found") | |
| return | |
| } | |
| if (!isConnected) { | |
| console.log("Not connected to QZ Tray, attempting to reconnect...") | |
| await connectToQZ() | |
| return | |
| } | |
| fetchPrinters() | |
| } | |
| const checkQzStatus = () => { | |
| console.log("Checking QZ Tray status...") | |
| console.log("QZ object exists:", !!window.qz) | |
| if (window.qz) { | |
| console.log("QZ version:", window.qz.version) | |
| window.qz.websocket | |
| .isActive() | |
| .then((active: boolean) => { | |
| console.log("QZ websocket active:", active) | |
| }) | |
| .catch((err: any) => { | |
| console.error("Error checking QZ websocket status:", err) | |
| }) | |
| } | |
| console.log("Protocol:", window.location.protocol) | |
| console.log("Is secure context:", window.isSecureContext) | |
| alert(`QZ Status Check: See browser console for details. | |
| QZ object exists: ${!!window.qz} | |
| Protocol: ${window.location.protocol} | |
| Secure context: ${window.isSecureContext}`) | |
| } | |
| const manualLoadQzTray = () => { | |
| console.log("Attempting manual QZ Tray load...") | |
| // Check if QZ is already available | |
| if (window.qz) { | |
| console.log("QZ Tray already available") | |
| setIsQzLoaded(true) | |
| connectToQZ() | |
| return | |
| } | |
| // Try to load from a local file or different source | |
| const script = document.createElement("script") | |
| script.src = "https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.min.js" // Try minified version | |
| script.async = true | |
| script.onload = () => { | |
| console.log("Manual QZ Tray load successful") | |
| setIsQzLoaded(true) | |
| setConnectionError(null) | |
| setTimeout(() => connectToQZ(), 500) | |
| } | |
| script.onerror = () => { | |
| console.error("Manual QZ Tray load failed") | |
| setConnectionError( | |
| "Failed to load QZ Tray library. Please ensure you have internet access or download QZ Tray locally.", | |
| ) | |
| } | |
| document.head.appendChild(script) | |
| } | |
| return ( | |
| <div className="min-h-screen bg-gray-50 p-4"> | |
| <div className="max-w-2xl mx-auto space-y-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Printer className="h-5 w-5" /> | |
| Talal Online Grocery - Receipt Printer | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| {/* Connection Status */} | |
| <div className="flex items-center gap-2 p-3 rounded-lg bg-gray-100"> | |
| {isConnected ? ( | |
| <> | |
| <Wifi className="h-4 w-4 text-green-600" /> | |
| <span className="text-green-600 font-medium">Connected to QZ Tray</span> | |
| </> | |
| ) : ( | |
| <> | |
| <WifiOff className="h-4 w-4 text-red-600" /> | |
| <span className="text-red-600 font-medium">Not connected to QZ Tray</span> | |
| </> | |
| )} | |
| </div> | |
| {/* Connection Error */} | |
| {connectionError && ( | |
| <div className="flex items-start gap-2 p-3 rounded-lg bg-red-50 text-red-800"> | |
| <AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" /> | |
| <div> | |
| <p className="font-medium">Connection Error</p> | |
| <p className="text-sm">{connectionError}</p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Connection Actions */} | |
| <div className="flex gap-2"> | |
| <Button onClick={connectToQZ} variant="outline" size="sm" className="flex-1"> | |
| <RefreshCw className="h-4 w-4 mr-2" /> | |
| Reconnect to QZ | |
| </Button> | |
| <Button onClick={checkQzStatus} variant="outline" size="sm" className="flex-1"> | |
| Check QZ Status | |
| </Button> | |
| <Button onClick={manualLoadQzTray} variant="outline" size="sm" className="flex-1"> | |
| Reload QZ Script | |
| </Button> | |
| </div> | |
| {/* BIXOLON Printer Status */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 p-3 rounded-lg bg-blue-50"> | |
| <Printer className="h-4 w-4 text-blue-600" /> | |
| <span className="font-medium">BIXOLON SRP-350III Status:</span> | |
| {hasBixolonPrinter === true && <span className="text-green-600 font-medium">✅ Found</span>} | |
| {hasBixolonPrinter === false && <span className="text-red-600 font-medium">❌ Not Found</span>} | |
| {hasBixolonPrinter === null && <span className="text-gray-600 font-medium">🔍 Checking...</span>} | |
| </div> | |
| <Button onClick={refreshPrinters} variant="outline" size="sm" className="w-full"> | |
| Refresh Printer Connection | |
| </Button> | |
| </div> | |
| {/* Current Configuration Display */} | |
| <div className="bg-green-50 p-3 rounded-lg"> | |
| <div className="text-sm text-green-800"> | |
| <div className="font-medium mb-1">Printer Configuration:</div> | |
| <div className="text-xs font-mono"> | |
| Printer: {selectedPrinter || "BIXOLON SRP-350III"} | |
| <br /> | |
| Paper: 80×300mm, No margins, Full cut enabled | |
| <br /> | |
| Format: Raw text, 1 copy, Standard density | |
| </div> | |
| </div> | |
| </div> | |
| {/* Debug Information */} | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">Debug Information:</label> | |
| <div className="bg-gray-900 text-green-400 p-3 rounded-lg font-mono text-xs whitespace-pre-line max-h-40 overflow-y-auto"> | |
| {debugInfo || "No debug information available yet."} | |
| </div> | |
| </div> | |
| {/* Print Button */} | |
| <Button onClick={printReceipt} disabled={!isConnected || isLoading} className="w-full" size="lg"> | |
| <Printer className="h-4 w-4 mr-2" /> | |
| {isLoading ? "Printing..." : "Print Receipt"} | |
| </Button> | |
| {!isConnected && ( | |
| <div className="text-sm text-gray-600 p-3 bg-yellow-50 rounded-lg"> | |
| <strong>Note:</strong> Make sure QZ Tray is installed and running on your computer. | |
| <br /> | |
| Download it from:{" "} | |
| <a | |
| href="https://qz.io/download/" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-600 underline" | |
| > | |
| qz.io/download | |
| </a> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {/* Receipt Preview */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Receipt Preview</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="bg-white p-4 border rounded-lg font-mono text-sm whitespace-pre-line">{receiptData}</div> | |
| </CardContent> | |
| </Card> | |
| {/* Setup Instructions */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Setup Instructions</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-3 text-sm"> | |
| <div> | |
| <strong>1. Install QZ Tray:</strong> | |
| <p className="text-gray-600"> | |
| Download and install QZ Tray from{" "} | |
| <a | |
| href="https://qz.io/download/" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-600 underline" | |
| > | |
| qz.io/download | |
| </a> | |
| </p> | |
| </div> | |
| <div> | |
| <strong>2. Connect BIXOLON SRP-350III:</strong> | |
| <p className="text-gray-600"> | |
| Make sure your BIXOLON SRP-350III thermal printer is connected and powered on | |
| </p> | |
| </div> | |
| <div> | |
| <strong>3. Start QZ Tray:</strong> | |
| <p className="text-gray-600">Launch the QZ Tray application on your computer</p> | |
| </div> | |
| <div> | |
| <strong>4. Print Receipt:</strong> | |
| <p className="text-gray-600"> | |
| Once connected, click "Print Receipt" to print with automatic paper cutting | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| ) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
