658bc6c0c9
Neue Ereignisse starten mit der aktuellen Uhrzeit; Datums-/Zeitanzeigen und Zeit-Picker nutzen durchgängig das 24-Stunden-Format. Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
8.0 KiB
TypeScript
276 lines
8.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Check } from 'lucide-react'
|
|
import SignaturePad from './SignaturePad.tsx'
|
|
import PasskeySignButton from './PasskeySignButton.tsx'
|
|
import type { PasskeySignature, SignatureValue } from '../types/signatures.js'
|
|
import { isPasskeySignature, getSignaturePayload, getSignatureAttribution } from '../utils/signatures.js'
|
|
import { formatAppDateTime } from '../utils/dateTimeFormat.js'
|
|
|
|
type SignatureMode = 'passkey' | 'classic'
|
|
|
|
interface SignatureSectionProps {
|
|
readOnly?: boolean
|
|
disabled?: boolean
|
|
isOnline: boolean
|
|
canSignSkipper: boolean
|
|
canSignCrew: boolean
|
|
signSkipper: SignatureValue | ''
|
|
signCrew: SignatureValue | ''
|
|
skipperSignatureValid: boolean
|
|
crewSignatureValid: boolean
|
|
onSignSkipperChange: (value: SignatureValue | '') => void
|
|
onSignCrewChange: (value: SignatureValue | '') => void
|
|
onPasskeySignSkipper: () => Promise<void>
|
|
onPasskeySignCrew: () => Promise<void>
|
|
onBeforeSign?: () => Promise<boolean>
|
|
}
|
|
|
|
function SignerAttributionBadge({ value }: { value: SignatureValue | '' }) {
|
|
const { t, i18n } = useTranslation()
|
|
const attribution = getSignatureAttribution(value)
|
|
if (!attribution) return null
|
|
|
|
const formattedDate = formatAppDateTime(attribution.signedAt, i18n.language)
|
|
|
|
return (
|
|
<div className="passkey-sign-badge valid signature-attribution-badge">
|
|
<span>{t('logs.sign_passkey_signed', { username: attribution.username })}</span>
|
|
<span className="passkey-sign-date">{formattedDate}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function padValue(value: SignatureValue | ''): string {
|
|
return getSignaturePayload(value)
|
|
}
|
|
|
|
function modeFromValue(value: SignatureValue | '', passkeyAvailable: boolean): SignatureMode {
|
|
if (isPasskeySignature(value)) return 'passkey'
|
|
if (getSignaturePayload(value)) return 'classic'
|
|
return passkeyAvailable ? 'passkey' : 'classic'
|
|
}
|
|
|
|
interface RoleSignatureBlockProps {
|
|
roleLabel: string
|
|
passkeyLabel: string
|
|
padId: string
|
|
value: SignatureValue | ''
|
|
passkeySignature?: PasskeySignature
|
|
signatureValid: boolean
|
|
showPasskey: boolean
|
|
readOnly: boolean
|
|
disabled: boolean
|
|
classicHint?: string
|
|
offlineHint?: string
|
|
onChange: (value: SignatureValue | '') => void
|
|
onPasskeySign: () => Promise<void>
|
|
onBeforeSign?: () => Promise<boolean>
|
|
}
|
|
|
|
function RoleSignatureBlock({
|
|
roleLabel,
|
|
passkeyLabel,
|
|
padId,
|
|
value,
|
|
passkeySignature,
|
|
signatureValid,
|
|
showPasskey,
|
|
readOnly,
|
|
disabled,
|
|
classicHint,
|
|
offlineHint,
|
|
onChange,
|
|
onPasskeySign,
|
|
onBeforeSign
|
|
}: RoleSignatureBlockProps) {
|
|
const { t } = useTranslation()
|
|
const [mode, setMode] = useState<SignatureMode>(() => modeFromValue(value, showPasskey))
|
|
|
|
useEffect(() => {
|
|
setMode(modeFromValue(value, showPasskey))
|
|
}, [value, showPasskey])
|
|
|
|
const switchToClassic = () => {
|
|
setMode('classic')
|
|
if (isPasskeySignature(value)) onChange('')
|
|
}
|
|
|
|
const switchToPasskey = () => {
|
|
setMode('passkey')
|
|
if (value && !isPasskeySignature(value)) onChange('')
|
|
}
|
|
|
|
const handlePadChange = (next: string) => {
|
|
setMode('classic')
|
|
onChange(next)
|
|
}
|
|
|
|
if (readOnly) {
|
|
if (isPasskeySignature(value)) {
|
|
return (
|
|
<div className="signature-role-block">
|
|
<PasskeySignButton
|
|
label={passkeyLabel}
|
|
signature={value}
|
|
signatureValid={signatureValid}
|
|
disabled={disabled}
|
|
canSign={false}
|
|
onSign={onPasskeySign}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div className="signature-role-block">
|
|
<SignerAttributionBadge value={value} />
|
|
<SignaturePad
|
|
id={padId}
|
|
label={roleLabel}
|
|
value={padValue(value)}
|
|
onChange={() => {}}
|
|
disabled={disabled}
|
|
readOnly
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const showPasskeyPanel = showPasskey && mode === 'passkey'
|
|
const showClassicPanel = !showPasskey || mode === 'classic'
|
|
|
|
return (
|
|
<div className="signature-role-block">
|
|
{showPasskey && (
|
|
<div className="signature-mode-toggle" role="tablist" aria-label={passkeyLabel}>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={mode === 'passkey'}
|
|
className={`signature-mode-btn ${mode === 'passkey' ? 'active' : ''}`}
|
|
onClick={switchToPasskey}
|
|
>
|
|
{t('logs.sign_mode_passkey')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={mode === 'classic'}
|
|
className={`signature-mode-btn ${mode === 'classic' ? 'active' : ''}`}
|
|
onClick={switchToClassic}
|
|
>
|
|
{t('logs.sign_mode_classic')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{showPasskeyPanel && (
|
|
<PasskeySignButton
|
|
label={passkeyLabel}
|
|
signature={passkeySignature}
|
|
signatureValid={signatureValid}
|
|
disabled={disabled}
|
|
canSign
|
|
onSign={onPasskeySign}
|
|
onClear={passkeySignature ? switchToClassic : undefined}
|
|
/>
|
|
)}
|
|
|
|
{showClassicPanel && (
|
|
<>
|
|
<SignerAttributionBadge value={value} />
|
|
<SignaturePad
|
|
id={padId}
|
|
label={roleLabel}
|
|
value={padValue(value)}
|
|
onChange={handlePadChange}
|
|
disabled={disabled}
|
|
readOnly={false}
|
|
onBeforeSign={onBeforeSign}
|
|
/>
|
|
{classicHint && !passkeySignature && (
|
|
<p className="signature-hint">{classicHint}</p>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{offlineHint && !showPasskey && (
|
|
<p className="signature-hint">{offlineHint}</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function SignatureSection({
|
|
readOnly = false,
|
|
disabled = false,
|
|
isOnline,
|
|
canSignSkipper,
|
|
canSignCrew,
|
|
signSkipper,
|
|
signCrew,
|
|
skipperSignatureValid,
|
|
crewSignatureValid,
|
|
onSignSkipperChange,
|
|
onSignCrewChange,
|
|
onPasskeySignSkipper,
|
|
onPasskeySignCrew,
|
|
onBeforeSign
|
|
}: SignatureSectionProps) {
|
|
const { t } = useTranslation()
|
|
|
|
const showSkipperPasskey = canSignSkipper && isOnline
|
|
const showCrewPasskey = canSignCrew && isOnline
|
|
const hasSignature = !!(signSkipper || signCrew)
|
|
|
|
return (
|
|
<div className="form-card">
|
|
<div className="form-header">
|
|
<Check size={20} className="form-icon" />
|
|
<h3>{t('logs.signatures')}</h3>
|
|
</div>
|
|
|
|
{!readOnly && (
|
|
<p className={`signature-lock-notice ${hasSignature ? 'locked' : ''}`}>
|
|
{hasSignature ? t('logs.sign_lock_active') : t('logs.sign_lock_notice')}
|
|
</p>
|
|
)}
|
|
|
|
<div className="form-grid signature-grid">
|
|
<RoleSignatureBlock
|
|
roleLabel={t('logs.sign_skipper')}
|
|
passkeyLabel={t('logs.sign_skipper')}
|
|
padId="sign-skipper"
|
|
value={signSkipper}
|
|
passkeySignature={isPasskeySignature(signSkipper) ? signSkipper : undefined}
|
|
signatureValid={skipperSignatureValid}
|
|
showPasskey={showSkipperPasskey}
|
|
readOnly={readOnly || !canSignSkipper}
|
|
disabled={disabled}
|
|
classicHint={showSkipperPasskey ? t('logs.sign_classic_or_passkey') : undefined}
|
|
offlineHint={!isOnline && canSignSkipper ? t('logs.sign_offline_hint') : undefined}
|
|
onChange={onSignSkipperChange}
|
|
onPasskeySign={onPasskeySignSkipper}
|
|
onBeforeSign={onBeforeSign}
|
|
/>
|
|
|
|
<RoleSignatureBlock
|
|
roleLabel={t('logs.sign_crew')}
|
|
passkeyLabel={t('logs.sign_crew')}
|
|
padId="sign-crew"
|
|
value={signCrew}
|
|
passkeySignature={isPasskeySignature(signCrew) ? signCrew : undefined}
|
|
signatureValid={crewSignatureValid}
|
|
showPasskey={showCrewPasskey}
|
|
readOnly={readOnly || !canSignCrew}
|
|
disabled={disabled}
|
|
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
|
|
onChange={onSignCrewChange}
|
|
onPasskeySign={onPasskeySignCrew}
|
|
onBeforeSign={onBeforeSign}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|