ca-grow-ops-manager/frontend/src/lib/qrcode.ts
fullsizemalt 4663b0ac86
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Navigation refactor with RBAC, DevTools for quick user switching, enhanced seed data
- Refactored navigation with grouped sections (Operations, Cultivation, Analytics, etc.)
- Added RBAC-based navigation filtering by user role
- Created DevTools panel for quick user switching during testing
- Added collapsible sidebar sections on desktop
- Mobile: bottom nav bar (4 items + More) with slide-up sheet
- Enhanced seed data with [DEMO] prefix markers
- Added multiple demo users: Owner, Manager, Cultivator, Worker
- Fixed domain to runfoo.run
- Added Audit Log and SOP Library pages to navigation
- Created usePermissions hook and RoleBadge component
2025-12-11 11:07:22 -08:00

233 lines
6.3 KiB
TypeScript

import QRCode from 'qrcode';
/**
* QR Code generation and printing utilities
*/
export interface QRCodeOptions {
width?: number;
margin?: number;
color?: {
dark?: string;
light?: string;
};
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
}
const defaultOptions: QRCodeOptions = {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff',
},
errorCorrectionLevel: 'M',
};
/**
* Generate QR code as data URL (PNG image)
*/
export async function generateQRCode(data: string, options: QRCodeOptions = {}): Promise<string> {
const opts = { ...defaultOptions, ...options };
try {
return await QRCode.toDataURL(data, {
width: opts.width,
margin: opts.margin,
color: opts.color,
errorCorrectionLevel: opts.errorCorrectionLevel,
});
} catch (error) {
console.error('QR Code generation failed:', error);
throw error;
}
}
/**
* Generate QR code as SVG string
*/
export async function generateQRCodeSVG(data: string, options: QRCodeOptions = {}): Promise<string> {
const opts = { ...defaultOptions, ...options };
try {
return await QRCode.toString(data, {
type: 'svg',
width: opts.width,
margin: opts.margin,
color: opts.color,
errorCorrectionLevel: opts.errorCorrectionLevel,
});
} catch (error) {
console.error('QR Code SVG generation failed:', error);
throw error;
}
}
/**
* Generate batch QR code data
*/
export function generateBatchQRData(batchId: string, batchName?: string): string {
const baseUrl = window.location.origin;
return JSON.stringify({
type: 'batch',
id: batchId,
name: batchName,
url: `${baseUrl}/batches/${batchId}`,
ts: Date.now(),
});
}
/**
* Generate plant QR code data
*/
export function generatePlantQRData(plantId: string, tagNumber: string, address: string): string {
const baseUrl = window.location.origin;
return JSON.stringify({
type: 'plant',
id: plantId,
tag: tagNumber,
address,
url: `${baseUrl}/plants/${plantId}`,
ts: Date.now(),
});
}
/**
* Generate visitor badge QR code data
*/
export function generateVisitorQRData(visitorId: string, badgeNumber: string, name: string): string {
return JSON.stringify({
type: 'visitor',
id: visitorId,
badge: badgeNumber,
name,
ts: Date.now(),
});
}
/**
* Generate room/zone QR code data
*/
export function generateRoomQRData(roomId: string, roomName: string): string {
const baseUrl = window.location.origin;
return JSON.stringify({
type: 'room',
id: roomId,
name: roomName,
url: `${baseUrl}/rooms/${roomId}`,
ts: Date.now(),
});
}
/**
* Parse scanned QR code data
*/
export function parseQRData(data: string): {
type: 'batch' | 'plant' | 'visitor' | 'room' | 'unknown';
id?: string;
url?: string;
[key: string]: any;
} {
try {
const parsed = JSON.parse(data);
return {
type: parsed.type || 'unknown',
...parsed,
};
} catch {
// If not JSON, try to extract URL
if (data.startsWith('http')) {
return { type: 'unknown', url: data };
}
return { type: 'unknown', raw: data };
}
}
/**
* Generate printable label HTML with QR code
*/
export async function generatePrintableLabel(params: {
type: 'batch' | 'plant' | 'visitor' | 'room';
title: string;
subtitle?: string;
details?: string[];
qrData: string;
size?: 'small' | 'medium' | 'large';
}): Promise<string> {
const { type, title, subtitle, details = [], qrData, size = 'medium' } = params;
const qrCodeDataUrl = await generateQRCode(qrData, {
width: size === 'small' ? 100 : size === 'large' ? 300 : 200,
});
const sizes = {
small: { width: '2in', fontSize: '8pt', qrSize: '1in' },
medium: { width: '3in', fontSize: '10pt', qrSize: '1.5in' },
large: { width: '4in', fontSize: '12pt', qrSize: '2in' },
};
const sizeConfig = sizes[size];
return `
<div style="
width: ${sizeConfig.width};
padding: 0.25in;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: ${sizeConfig.fontSize};
border: 1px solid #ccc;
display: flex;
align-items: center;
gap: 0.25in;
">
<img src="${qrCodeDataUrl}" style="width: ${sizeConfig.qrSize}; height: ${sizeConfig.qrSize};" />
<div style="flex: 1;">
<div style="font-weight: bold; font-size: 1.2em; margin-bottom: 4px;">
${title}
</div>
${subtitle ? `<div style="color: #666; margin-bottom: 4px;">${subtitle}</div>` : ''}
${details.map(d => `<div style="font-size: 0.9em;">${d}</div>`).join('')}
<div style="font-size: 0.7em; color: #999; margin-top: 4px;">
${type.toUpperCase()}${new Date().toLocaleDateString()}
</div>
</div>
</div>
`;
}
/**
* Print label(s) using browser print dialog
*/
export function printLabels(labelsHtml: string[], title: string = 'Print Labels') {
const printWindow = window.open('', '_blank');
if (!printWindow) {
alert('Please allow popups to print labels');
return;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
@page { size: auto; margin: 0.5in; }
@media print {
.label { page-break-inside: avoid; margin-bottom: 0.25in; }
}
body { margin: 0; padding: 0.5in; }
.label { margin-bottom: 0.5in; }
</style>
</head>
<body>
${labelsHtml.map(html => `<div class="label">${html}</div>`).join('')}
<script>
window.onload = function() {
window.print();
window.onafterprint = function() { window.close(); };
};
</script>
</body>
</html>
`);
printWindow.document.close();
}