init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
'use client';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Html5Qrcode } from 'html5-qrcode';
import { X, Camera, AlertCircle, ZoomIn, ZoomOut } from 'lucide-react';
interface BarcodeScannerProps {
onScan: (value: string) => void;
onClose: () => void;
}
export default function BarcodeScanner({ onScan, onClose }: BarcodeScannerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState('');
const [zoom, setZoom] = useState(1);
const [zoomMax, setZoomMax] = useState(3);
const [supportsZoom, setSupportsZoom] = useState(false);
const initializedRef = useRef(false);
const scannerRef = useRef<Html5Qrcode | null>(null);
const trackRef = useRef<MediaStreamTrack | null>(null);
const applyZoom = useCallback(async (level: number, video?: HTMLVideoElement | null) => {
const v = video || (document.querySelector('#sr-barcode-scanner video') as HTMLVideoElement | null);
if (v && v.srcObject) {
if (trackRef.current && supportsZoom) {
try { await trackRef.current.applyConstraints({ advanced: [{ zoom: level }] as any }); } catch { }
}
(v as HTMLElement).style.transform = `scale(${level})`;
}
}, [supportsZoom]);
useEffect(() => {
// StrictMode guard: skip second effect run
if (initializedRef.current) return;
initializedRef.current = true;
const el = containerRef.current;
if (!el) return;
const id = 'sr-barcode-scanner';
el.id = id;
el.innerHTML = '';
let cancelled = false;
const s = new Html5Qrcode(id);
scannerRef.current = s;
const startWithFacing = async (facingMode: 'environment' | 'user') => {
await s.start(
{ facingMode },
{ fps: 30 },
(decodedText) => {
if (cancelled) return;
try { s.stop(); } catch { }
onScan(decodedText);
},
() => {}
);
// Set up zoom after camera starts
setTimeout(() => {
if (cancelled) return;
const video = el.querySelector('video');
if (video && video.srcObject) {
(video as HTMLElement).style.transition = 'transform 0.2s';
(video as HTMLElement).style.transformOrigin = 'center center';
const track = (video.srcObject as MediaStream).getVideoTracks()[0];
if (track) {
trackRef.current = track;
const caps = track.getCapabilities() as any;
if (caps?.zoom) { setSupportsZoom(true); setZoomMax(caps.zoom.max || 3); }
}
}
}, 300);
};
startWithFacing('environment').catch(() => {
return startWithFacing('user');
}).catch((err: unknown) => {
if (cancelled) return;
const msg = err instanceof Error ? err.message : String(err);
const isIOS = navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad');
if (msg.includes('NotAllowed') || msg.includes('Permission')) {
setError(isIOS ? 'Camera denied. Settings → Safari → Camera → Allow.' : 'Camera permission denied.');
} else if (msg.includes('NotFound')) {
setError('No camera found on this device.');
} else {
setError(msg);
}
});
return () => {
cancelled = true;
try { s.stop(); } catch { }
};
}, []);
const handleZoomIn = () => {
const newZoom = Math.min(zoom + 0.5, zoomMax);
setZoom(newZoom);
applyZoom(newZoom);
};
const handleZoomOut = () => {
const newZoom = Math.max(zoom - 0.5, 0.5);
setZoom(newZoom);
applyZoom(newZoom);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl overflow-hidden w-full max-w-sm mx-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200">
<div className="flex items-center gap-2">
<Camera className="w-4 h-5 text-primary" />
<span className="text-sm font-bold text-slate-900">Scan Barcode</span>
</div>
<button onClick={onClose} className="p-1 hover:bg-slate-100 rounded-lg transition-colors">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
{error ? (
<div className="p-6 text-center space-y-4">
<div className="w-12 h-12 mx-auto rounded-full bg-red-50 flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-red-500" />
</div>
<p className="text-sm text-slate-600">{error}</p>
<button onClick={onClose} className="px-6 py-2.5 bg-primary text-white rounded-xl text-sm font-medium">Close</button>
</div>
) : (
<>
<div className="relative w-full aspect-[4/3] overflow-hidden">
<div ref={containerRef} className="absolute inset-0 bg-black" />
<div className="absolute bottom-3 right-3 flex items-center gap-1 bg-black/60 rounded-full px-2 py-1.5 z-10">
<button onClick={handleZoomOut} className="p-1 text-white/70 hover:text-white" type="button"><ZoomOut className="w-4 h-4" /></button>
<span className="text-[10px] text-white font-mono min-w-[24px] text-center">{zoom.toFixed(1)}x</span>
<button onClick={handleZoomIn} className="p-1 text-white/70 hover:text-white" type="button"><ZoomIn className="w-4 h-4" /></button>
</div>
</div>
<p className="text-[11px] text-slate-500 text-center py-3 px-4">Position the barcode or QR code within the scanning area</p>
</>
)}
</div>
</div>
);
}