- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints. - Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete. - Created VendorContext for managing vendor state across components. - Developed VendorGeneralInfo component to display detailed vendor information. - Introduced AedSymbol and Money components for consistent currency representation. - Added PromptDialog for user input prompts throughout the application. - Implemented RelationLink component for unified related-data display in CRUD tables. - Created InspectionTemplatesClient for API interactions related to inspection templates.
120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { Eraser, Save } from "lucide-react"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
|
|
type Props = {
|
|
label: string
|
|
existingUrl?: string | null
|
|
onSave: (dataUrl: string) => Promise<void> | void
|
|
saving?: boolean
|
|
}
|
|
|
|
/**
|
|
* Minimal in-house signature pad — uses <canvas> + pointer events.
|
|
* Works with mouse, touch, and stylus on any device. No external dep.
|
|
*/
|
|
export function SignaturePad({ label, existingUrl, onSave, saving }: Props) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const drawingRef = useRef(false)
|
|
const lastPointRef = useRef<{ x: number; y: number } | null>(null)
|
|
const [hasDrawn, setHasDrawn] = useState(false)
|
|
|
|
const getCtx = () => {
|
|
const c = canvasRef.current
|
|
if (!c) return null
|
|
const ctx = c.getContext("2d")
|
|
if (!ctx) return null
|
|
ctx.lineWidth = 2.2
|
|
ctx.lineCap = "round"
|
|
ctx.lineJoin = "round"
|
|
ctx.strokeStyle = "#111"
|
|
return ctx
|
|
}
|
|
|
|
useEffect(() => {
|
|
// Size the canvas to its CSS size while keeping crisp lines on HiDPI screens.
|
|
const c = canvasRef.current
|
|
if (!c) return
|
|
const dpr = window.devicePixelRatio || 1
|
|
const rect = c.getBoundingClientRect()
|
|
c.width = Math.round(rect.width * dpr)
|
|
c.height = Math.round(rect.height * dpr)
|
|
const ctx = c.getContext("2d")
|
|
ctx?.scale(dpr, dpr)
|
|
}, [])
|
|
|
|
const pointFromEvent = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect()
|
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
|
}
|
|
|
|
const handleDown = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault()
|
|
;(e.target as HTMLCanvasElement).setPointerCapture(e.pointerId)
|
|
drawingRef.current = true
|
|
lastPointRef.current = pointFromEvent(e)
|
|
}
|
|
const handleMove = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
if (!drawingRef.current) return
|
|
const ctx = getCtx()
|
|
if (!ctx || !lastPointRef.current) return
|
|
const p = pointFromEvent(e)
|
|
ctx.beginPath()
|
|
ctx.moveTo(lastPointRef.current.x, lastPointRef.current.y)
|
|
ctx.lineTo(p.x, p.y)
|
|
ctx.stroke()
|
|
lastPointRef.current = p
|
|
setHasDrawn(true)
|
|
}
|
|
const handleUp = () => {
|
|
drawingRef.current = false
|
|
lastPointRef.current = null
|
|
}
|
|
|
|
const clear = () => {
|
|
const c = canvasRef.current
|
|
const ctx = getCtx()
|
|
if (!c || !ctx) return
|
|
ctx.clearRect(0, 0, c.width, c.height)
|
|
setHasDrawn(false)
|
|
}
|
|
|
|
const submit = async () => {
|
|
if (!canvasRef.current || !hasDrawn) return
|
|
const dataUrl = canvasRef.current.toDataURL("image/png")
|
|
await onSave(dataUrl)
|
|
}
|
|
|
|
return (
|
|
<div className="rounded border bg-white">
|
|
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/30">
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<div className="flex items-center gap-1">
|
|
<Button type="button" variant="ghost" size="sm" onClick={clear} disabled={saving}>
|
|
<Eraser className="size-3.5" /> Clear
|
|
</Button>
|
|
<Button type="button" size="sm" onClick={submit} disabled={!hasDrawn || saving}>
|
|
<Save className="size-3.5" /> {saving ? "Saving…" : "Save"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{existingUrl && !hasDrawn && (
|
|
<div className="px-3 pt-2 text-xs text-muted-foreground">
|
|
Previously signed:
|
|
<img src={existingUrl} alt="" className="block max-h-20 mt-1" />
|
|
</div>
|
|
)}
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="block w-full h-32 touch-none bg-white"
|
|
onPointerDown={handleDown}
|
|
onPointerMove={handleMove}
|
|
onPointerUp={handleUp}
|
|
onPointerCancel={handleUp}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|