Files
thetool/public/mobile/modules/lager/shippingnote/SignaturePad.js
2026-01-17 12:48:08 +00:00

301 lines
11 KiB
JavaScript

/**
* SignaturePad Component
*
* Full-screen signature capture for shipping notes.
* Features:
* - Canvas-based signature drawing
* - Customer name input
* - Clear/retry functionality
* - Base64 PNG export
*/
import { shippingNoteApi } from '/mobile/modules/lager/shippingnote/ShippingNoteModule.js';
export default {
name: 'SignaturePad',
emits: ['close', 'signed', 'toast'],
props: {
shippingNoteId: [Number, String],
shippingNote: Object
},
setup(props, { emit }) {
const { ref, onMounted, onUnmounted, nextTick } = Vue;
// Refs
const canvasRef = ref(null);
const signatureName = ref('');
const loading = ref(false);
const hasSignature = ref(false);
// Canvas context
let ctx = null;
let isDrawing = false;
let lastX = 0;
let lastY = 0;
// Initialize canvas
onMounted(async () => {
await nextTick();
initCanvas();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
const initCanvas = () => {
const canvas = canvasRef.value;
if (!canvas) return;
// Set canvas size to container
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000000';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Fill with white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const handleResize = () => {
// Save current signature
const canvas = canvasRef.value;
if (!canvas) return;
const imageData = canvas.toDataURL();
// Resize canvas
initCanvas();
// Restore signature
if (hasSignature.value) {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0);
};
img.src = imageData;
}
};
// Get position from touch/mouse event
const getPosition = (e) => {
const canvas = canvasRef.value;
const rect = canvas.getBoundingClientRect();
if (e.touches && e.touches.length > 0) {
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top
};
}
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
};
// Start drawing
const startDrawing = (e) => {
e.preventDefault();
isDrawing = true;
const pos = getPosition(e);
lastX = pos.x;
lastY = pos.y;
};
// Draw
const draw = (e) => {
if (!isDrawing) return;
e.preventDefault();
const pos = getPosition(e);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
lastX = pos.x;
lastY = pos.y;
hasSignature.value = true;
};
// Stop drawing
const stopDrawing = () => {
isDrawing = false;
};
// Clear canvas
const clearCanvas = () => {
const canvas = canvasRef.value;
if (!canvas || !ctx) return;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
hasSignature.value = false;
navigator.vibrate?.([30]);
};
// Submit signature
const submitSignature = async () => {
if (!hasSignature.value) {
emit('toast', 'Bitte unterschreiben', 'error');
return;
}
if (!signatureName.value.trim()) {
emit('toast', 'Bitte Namen eingeben', 'error');
return;
}
loading.value = true;
try {
const canvas = canvasRef.value;
const signatureData = canvas.toDataURL('image/png');
const data = await shippingNoteApi.post(`sign?id=${props.shippingNoteId}`, {
signature: signatureData,
signatureName: signatureName.value.trim()
});
if (data.success) {
emit('signed', data);
} else {
emit('toast', data.error || 'Fehler beim Speichern', 'error');
}
} catch (e) {
console.error('Signature submit failed:', e);
emit('toast', 'Netzwerkfehler', 'error');
} finally {
loading.value = false;
}
};
// Close handler
const handleClose = () => {
emit('close');
};
return {
canvasRef,
signatureName,
loading,
hasSignature,
startDrawing,
draw,
stopDrawing,
clearCanvas,
submitSignature,
handleClose
};
},
template: `
<div class="flex flex-col h-full bg-white dark:bg-slate-900">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
<button
@click="handleClose"
class="p-2 -ml-2 text-slate-500"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h2 class="text-lg font-semibold text-slate-800 dark:text-white">Unterschrift</h2>
<button
@click="clearCanvas"
class="p-2 -mr-2 text-red-500"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
<!-- Shipping Note Info -->
<div v-if="shippingNote" class="px-4 py-3 bg-slate-100 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
<div class="text-sm text-slate-500 dark:text-slate-400">Lieferschein Nr.</div>
<div class="font-medium text-slate-800 dark:text-white">{{ shippingNote.number || shippingNote.id }}</div>
<div v-if="shippingNote.customerName" class="text-sm text-slate-600 dark:text-slate-300 mt-1">
{{ shippingNote.customerName }}
</div>
</div>
<!-- Signature Canvas -->
<div class="flex-1 relative bg-white overflow-hidden">
<canvas
ref="canvasRef"
class="absolute inset-0 touch-none"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="startDrawing"
@touchmove="draw"
@touchend="stopDrawing"
@touchcancel="stopDrawing"
></canvas>
<!-- Signature line hint -->
<div class="absolute bottom-16 left-8 right-8 border-b-2 border-dashed border-slate-300 pointer-events-none"></div>
<div class="absolute bottom-12 left-8 text-xs text-slate-400 pointer-events-none">Hier unterschreiben</div>
<!-- Empty state hint -->
<div v-if="!hasSignature" class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="text-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
<p class="text-sm">Mit Finger unterschreiben</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="p-4 bg-slate-50 dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 space-y-3">
<!-- Name input -->
<div>
<label class="block text-xs text-slate-500 dark:text-slate-400 mb-1">Name des Unterzeichners *</label>
<input
type="text"
v-model="signatureName"
class="w-full px-4 py-3 bg-white dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-xl text-base"
placeholder="Max Mustermann"
/>
</div>
<!-- Submit button -->
<button
@click="submitSignature"
:disabled="!hasSignature || loading"
:class="[
'w-full py-5 rounded-xl text-base font-semibold transition flex items-center justify-center gap-2',
hasSignature && !loading
? 'bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg active:scale-[0.98]'
: 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500'
]"
>
<svg v-if="loading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ loading ? 'Wird gespeichert...' : 'Unterschrift speichern' }}
</button>
</div>
</div>
`
};