- 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
233 lines
6.3 KiB
TypeScript
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();
|
|
}
|