- Implement tests to ensure job skips execution on weekends, when penalties are zero, or when no payroll period exists. - Validate late penalty creation for late check-ins and ensure no penalties for on-time or early check-ins. - Test absent penalties for employees without attendance records. - Ensure no duplicate penalties are created and that payroll recalculations are accurate after penalties are applied. - Handle multiple employees and edge cases, including employees without associated users. - Verify that the job can be dispatched to the queue and has the correct retry configuration.
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Camera, RotateCcw, X } from 'lucide-react';
|
|
|
|
interface CameraCaptureProps {
|
|
onCapture: (dataUrl: string) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const startCamera = useCallback(async () => {
|
|
try {
|
|
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: 'user', width: 640, height: 480 },
|
|
});
|
|
setStream(mediaStream);
|
|
if (videoRef.current) {
|
|
videoRef.current.srcObject = mediaStream;
|
|
}
|
|
setError(null);
|
|
} catch {
|
|
setError('Tidak dapat mengakses kamera. Pastikan izin kamera diberikan.');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
startCamera();
|
|
return () => {
|
|
stream?.getTracks().forEach((track) => track.stop());
|
|
};
|
|
}, []);
|
|
|
|
const capture = () => {
|
|
if (!videoRef.current || !canvasRef.current) return;
|
|
|
|
const canvas = canvasRef.current;
|
|
const video = videoRef.current;
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.translate(canvas.width, 0);
|
|
ctx.scale(-1, 1);
|
|
ctx.drawImage(video, 0, 0);
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
|
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
|
setCapturedImage(dataUrl);
|
|
|
|
stream?.getTracks().forEach((track) => track.stop());
|
|
};
|
|
|
|
const retake = () => {
|
|
setCapturedImage(null);
|
|
startCamera();
|
|
};
|
|
|
|
const confirm = () => {
|
|
if (capturedImage) {
|
|
onCapture(capturedImage);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
|
<div className="relative w-full max-w-md rounded-lg bg-background p-4 shadow-lg">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold">Ambil Foto</h3>
|
|
<Button variant="ghost" size="icon" onClick={onClose}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{error ? (
|
|
<div className="flex flex-col items-center gap-4 py-8">
|
|
<p className="text-center text-sm text-muted-foreground">{error}</p>
|
|
<Button onClick={startCamera}>Coba Lagi</Button>
|
|
</div>
|
|
) : capturedImage ? (
|
|
<div className="flex flex-col gap-4">
|
|
<img src={capturedImage} alt="Captured" className="w-full rounded-lg" />
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" className="flex-1" onClick={retake}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
Ulangi
|
|
</Button>
|
|
<Button className="flex-1" onClick={confirm}>
|
|
Gunakan Foto
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="relative overflow-hidden rounded-lg bg-black">
|
|
<video
|
|
ref={videoRef}
|
|
autoPlay
|
|
playsInline
|
|
muted
|
|
className="w-full"
|
|
style={{ transform: 'scaleX(-1)' }}
|
|
/>
|
|
</div>
|
|
<Button className="w-full" onClick={capture}>
|
|
<Camera className="mr-2 h-4 w-4" />
|
|
Ambil Foto
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<canvas ref={canvasRef} className="hidden" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|