feat: Unterschriften bei Logbuchänderungen invalidieren
Änderungen am Eintrag (außer Fotos) entfernen Skipper- und Crew-Signaturen automatisch. Vor dem Unterschreiben erscheinen Hinweis-Banner und Bestätigung. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2220,6 +2220,23 @@ body:has(.theme-cupertino) {
|
|||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signature-lock-notice {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.25);
|
||||||
|
background: rgba(212, 175, 55, 0.08);
|
||||||
|
color: rgba(254, 243, 199, 0.95);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-lock-notice.locked {
|
||||||
|
border-color: rgba(34, 197, 94, 0.3);
|
||||||
|
background: rgba(34, 197, 94, 0.08);
|
||||||
|
color: rgba(220, 252, 231, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.passkey-sign-block {
|
.passkey-sign-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
normalizeSignature,
|
normalizeSignature,
|
||||||
serializeSignature,
|
serializeSignature,
|
||||||
isPasskeySignature,
|
isPasskeySignature,
|
||||||
isSignatureValidForEntry
|
isSignatureValidForEntry,
|
||||||
|
hasAnySignature
|
||||||
} from '../utils/signatures.js'
|
} from '../utils/signatures.js'
|
||||||
import type { SignatureValue } from '../types/signatures.js'
|
import type { SignatureValue } from '../types/signatures.js'
|
||||||
import { buildLogEntryPayload } from '../utils/logEntryPayload.js'
|
import { buildLogEntryPayload } from '../utils/logEntryPayload.js'
|
||||||
@@ -73,7 +74,7 @@ export default function LogEntryEditor({
|
|||||||
preloadedYacht
|
preloadedYacht
|
||||||
}: LogEntryEditorProps) {
|
}: LogEntryEditorProps) {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { showAlert } = useDialog()
|
const { showAlert, showConfirm } = useDialog()
|
||||||
|
|
||||||
// General details state
|
// General details state
|
||||||
const [date, setDate] = useState('')
|
const [date, setDate] = useState('')
|
||||||
@@ -141,6 +142,8 @@ export default function LogEntryEditor({
|
|||||||
const [dragOver, setDragOver] = useState(false)
|
const [dragOver, setDragOver] = useState(false)
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
const lockedContentHashRef = useRef<string | null>(null)
|
||||||
|
const contentReadyRef = useRef(false)
|
||||||
|
|
||||||
const applyTrackStats = (waypoints: SavedTrack['waypoints']) => {
|
const applyTrackStats = (waypoints: SavedTrack['waypoints']) => {
|
||||||
const stats = computeTrackStats(waypoints)
|
const stats = computeTrackStats(waypoints)
|
||||||
@@ -221,10 +224,56 @@ export default function LogEntryEditor({
|
|||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [buildPayloadForSigning])
|
}, [buildPayloadForSigning])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
contentReadyRef.current = false
|
||||||
|
if (loading) return
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
contentReadyRef.current = true
|
||||||
|
}, 0)
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!entryHash || !contentReadyRef.current || readOnly) return
|
||||||
|
|
||||||
|
const hasSig = hasAnySignature(signSkipper, signCrew)
|
||||||
|
if (!hasSig) {
|
||||||
|
lockedContentHashRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockedContentHashRef.current) {
|
||||||
|
lockedContentHashRef.current = entryHash
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryHash !== lockedContentHashRef.current) {
|
||||||
|
lockedContentHashRef.current = null
|
||||||
|
setSignSkipper('')
|
||||||
|
setSignCrew('')
|
||||||
|
void showAlert(
|
||||||
|
t('logs.sign_cleared_re_sign'),
|
||||||
|
t('logs.sign_cleared_re_sign_title')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [entryHash, signSkipper, signCrew, readOnly, showAlert, t])
|
||||||
|
|
||||||
|
const confirmSignWarning = useCallback(async (): Promise<boolean> => {
|
||||||
|
return showConfirm(
|
||||||
|
t('logs.sign_lock_warning'),
|
||||||
|
t('logs.sign_lock_warning_title'),
|
||||||
|
t('logs.sign_proceed'),
|
||||||
|
t('logs.sign_cancel')
|
||||||
|
)
|
||||||
|
}, [showConfirm, t])
|
||||||
|
|
||||||
const skipperSignatureValid = !isPasskeySignature(signSkipper) || isSignatureValidForEntry(signSkipper, entryHash)
|
const skipperSignatureValid = !isPasskeySignature(signSkipper) || isSignatureValidForEntry(signSkipper, entryHash)
|
||||||
const crewSignatureValid = !isPasskeySignature(signCrew) || isSignatureValidForEntry(signCrew, entryHash)
|
const crewSignatureValid = !isPasskeySignature(signCrew) || isSignatureValidForEntry(signCrew, entryHash)
|
||||||
|
|
||||||
const handlePasskeySignSkipper = async () => {
|
const handlePasskeySignSkipper = async () => {
|
||||||
|
const confirmed = await confirmSignWarning()
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
const hash = await hashEntryForSigning(buildPayloadForSigning())
|
const hash = await hashEntryForSigning(buildPayloadForSigning())
|
||||||
const signature = await signLogEntry({
|
const signature = await signLogEntry({
|
||||||
logbookId,
|
logbookId,
|
||||||
@@ -234,9 +283,13 @@ export default function LogEntryEditor({
|
|||||||
})
|
})
|
||||||
setSignSkipper(signature)
|
setSignSkipper(signature)
|
||||||
setEntryHash(hash)
|
setEntryHash(hash)
|
||||||
|
lockedContentHashRef.current = hash
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePasskeySignCrew = async () => {
|
const handlePasskeySignCrew = async () => {
|
||||||
|
const confirmed = await confirmSignWarning()
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
const hash = await hashEntryForSigning(buildPayloadForSigning())
|
const hash = await hashEntryForSigning(buildPayloadForSigning())
|
||||||
const signature = await signLogEntry({
|
const signature = await signLogEntry({
|
||||||
logbookId,
|
logbookId,
|
||||||
@@ -246,6 +299,7 @@ export default function LogEntryEditor({
|
|||||||
})
|
})
|
||||||
setSignCrew(signature)
|
setSignCrew(signature)
|
||||||
setEntryHash(hash)
|
setEntryHash(hash)
|
||||||
|
lockedContentHashRef.current = hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-calculate Freshwater Consumption
|
// Auto-calculate Freshwater Consumption
|
||||||
@@ -296,6 +350,8 @@ export default function LogEntryEditor({
|
|||||||
async function loadEntry() {
|
async function loadEntry() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
lockedContentHashRef.current = null
|
||||||
|
contentReadyRef.current = false
|
||||||
try {
|
try {
|
||||||
if (readOnly && preloadedEntry) {
|
if (readOnly && preloadedEntry) {
|
||||||
setDate(preloadedEntry.date || '')
|
setDate(preloadedEntry.date || '')
|
||||||
@@ -307,11 +363,13 @@ export default function LogEntryEditor({
|
|||||||
setFwMorning(String(preloadedEntry.freshwater.morning || 0))
|
setFwMorning(String(preloadedEntry.freshwater.morning || 0))
|
||||||
setFwRefilled(String(preloadedEntry.freshwater.refilled || 0))
|
setFwRefilled(String(preloadedEntry.freshwater.refilled || 0))
|
||||||
setFwEvening(String(preloadedEntry.freshwater.evening || 0))
|
setFwEvening(String(preloadedEntry.freshwater.evening || 0))
|
||||||
|
setFwConsumption(String(preloadedEntry.freshwater.consumption ?? 0))
|
||||||
}
|
}
|
||||||
if (preloadedEntry.fuel) {
|
if (preloadedEntry.fuel) {
|
||||||
setFuelMorning(String(preloadedEntry.fuel.morning || 0))
|
setFuelMorning(String(preloadedEntry.fuel.morning || 0))
|
||||||
setFuelRefilled(String(preloadedEntry.fuel.refilled || 0))
|
setFuelRefilled(String(preloadedEntry.fuel.refilled || 0))
|
||||||
setFuelEvening(String(preloadedEntry.fuel.evening || 0))
|
setFuelEvening(String(preloadedEntry.fuel.evening || 0))
|
||||||
|
setFuelConsumption(String(preloadedEntry.fuel.consumption ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setSignSkipper(normalizeSignature(preloadedEntry.signSkipper) || '')
|
setSignSkipper(normalizeSignature(preloadedEntry.signSkipper) || '')
|
||||||
@@ -337,11 +395,13 @@ export default function LogEntryEditor({
|
|||||||
setFwMorning(String(decrypted.freshwater.morning || 0))
|
setFwMorning(String(decrypted.freshwater.morning || 0))
|
||||||
setFwRefilled(String(decrypted.freshwater.refilled || 0))
|
setFwRefilled(String(decrypted.freshwater.refilled || 0))
|
||||||
setFwEvening(String(decrypted.freshwater.evening || 0))
|
setFwEvening(String(decrypted.freshwater.evening || 0))
|
||||||
|
setFwConsumption(String(decrypted.freshwater.consumption ?? 0))
|
||||||
}
|
}
|
||||||
if (decrypted.fuel) {
|
if (decrypted.fuel) {
|
||||||
setFuelMorning(String(decrypted.fuel.morning || 0))
|
setFuelMorning(String(decrypted.fuel.morning || 0))
|
||||||
setFuelRefilled(String(decrypted.fuel.refilled || 0))
|
setFuelRefilled(String(decrypted.fuel.refilled || 0))
|
||||||
setFuelEvening(String(decrypted.fuel.evening || 0))
|
setFuelEvening(String(decrypted.fuel.evening || 0))
|
||||||
|
setFuelConsumption(String(decrypted.fuel.consumption ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setSignSkipper(normalizeSignature(decrypted.signSkipper) || '')
|
setSignSkipper(normalizeSignature(decrypted.signSkipper) || '')
|
||||||
@@ -1404,6 +1464,7 @@ export default function LogEntryEditor({
|
|||||||
onSignCrewChange={setSignCrew}
|
onSignCrewChange={setSignCrew}
|
||||||
onPasskeySignSkipper={handlePasskeySignSkipper}
|
onPasskeySignSkipper={handlePasskeySignSkipper}
|
||||||
onPasskeySignCrew={handlePasskeySignCrew}
|
onPasskeySignCrew={handlePasskeySignCrew}
|
||||||
|
onBeforeSign={confirmSignWarning}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Save Controls */}
|
{/* Save Controls */}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface SignaturePadProps {
|
|||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
onBeforeSign?: () => Promise<boolean> | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const STROKE_COLOR = '#0f172a'
|
const STROKE_COLOR = '#0f172a'
|
||||||
@@ -21,7 +22,8 @@ export default function SignaturePad({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false
|
readOnly = false,
|
||||||
|
onBeforeSign
|
||||||
}: SignaturePadProps) {
|
}: SignaturePadProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -138,9 +140,15 @@ export default function SignaturePad({
|
|||||||
onChange(canvas.toDataURL('image/png'))
|
onChange(canvas.toDataURL('image/png'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {
|
const handlePointerDown = async (event: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
if (readOnly || disabled) return
|
if (readOnly || disabled) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!value && !hasInk.current && onBeforeSign) {
|
||||||
|
const allowed = await onBeforeSign()
|
||||||
|
if (!allowed) return
|
||||||
|
}
|
||||||
|
|
||||||
const point = getPoint(event)
|
const point = getPoint(event)
|
||||||
if (!point) return
|
if (!point) return
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface SignatureSectionProps {
|
|||||||
onSignCrewChange: (value: SignatureValue | '') => void
|
onSignCrewChange: (value: SignatureValue | '') => void
|
||||||
onPasskeySignSkipper: () => Promise<void>
|
onPasskeySignSkipper: () => Promise<void>
|
||||||
onPasskeySignCrew: () => Promise<void>
|
onPasskeySignCrew: () => Promise<void>
|
||||||
|
onBeforeSign?: () => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function padValue(value: SignatureValue | ''): string {
|
function padValue(value: SignatureValue | ''): string {
|
||||||
@@ -49,6 +50,7 @@ interface RoleSignatureBlockProps {
|
|||||||
offlineHint?: string
|
offlineHint?: string
|
||||||
onChange: (value: SignatureValue | '') => void
|
onChange: (value: SignatureValue | '') => void
|
||||||
onPasskeySign: () => Promise<void>
|
onPasskeySign: () => Promise<void>
|
||||||
|
onBeforeSign?: () => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoleSignatureBlock({
|
function RoleSignatureBlock({
|
||||||
@@ -64,7 +66,8 @@ function RoleSignatureBlock({
|
|||||||
classicHint,
|
classicHint,
|
||||||
offlineHint,
|
offlineHint,
|
||||||
onChange,
|
onChange,
|
||||||
onPasskeySign
|
onPasskeySign,
|
||||||
|
onBeforeSign
|
||||||
}: RoleSignatureBlockProps) {
|
}: RoleSignatureBlockProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [mode, setMode] = useState<SignatureMode>(() => modeFromValue(value, showPasskey))
|
const [mode, setMode] = useState<SignatureMode>(() => modeFromValue(value, showPasskey))
|
||||||
@@ -166,6 +169,7 @@ function RoleSignatureBlock({
|
|||||||
onChange={handlePadChange}
|
onChange={handlePadChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
|
onBeforeSign={onBeforeSign}
|
||||||
/>
|
/>
|
||||||
{classicHint && !passkeySignature && (
|
{classicHint && !passkeySignature && (
|
||||||
<p className="signature-hint">{classicHint}</p>
|
<p className="signature-hint">{classicHint}</p>
|
||||||
@@ -193,12 +197,14 @@ export default function SignatureSection({
|
|||||||
onSignSkipperChange,
|
onSignSkipperChange,
|
||||||
onSignCrewChange,
|
onSignCrewChange,
|
||||||
onPasskeySignSkipper,
|
onPasskeySignSkipper,
|
||||||
onPasskeySignCrew
|
onPasskeySignCrew,
|
||||||
|
onBeforeSign
|
||||||
}: SignatureSectionProps) {
|
}: SignatureSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const showSkipperPasskey = isOwner && isOnline
|
const showSkipperPasskey = isOwner && isOnline
|
||||||
const showCrewPasskey = hasWriteCollaborators && isOnline
|
const showCrewPasskey = hasWriteCollaborators && isOnline
|
||||||
|
const hasSignature = !!(signSkipper || signCrew)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-card">
|
<div className="form-card">
|
||||||
@@ -207,6 +213,12 @@ export default function SignatureSection({
|
|||||||
<h3>{t('logs.signatures')}</h3>
|
<h3>{t('logs.signatures')}</h3>
|
||||||
</div>
|
</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">
|
<div className="form-grid signature-grid">
|
||||||
<RoleSignatureBlock
|
<RoleSignatureBlock
|
||||||
roleLabel={t('logs.sign_skipper')}
|
roleLabel={t('logs.sign_skipper')}
|
||||||
@@ -222,6 +234,7 @@ export default function SignatureSection({
|
|||||||
offlineHint={!isOnline && isOwner ? t('logs.sign_offline_hint') : undefined}
|
offlineHint={!isOnline && isOwner ? t('logs.sign_offline_hint') : undefined}
|
||||||
onChange={onSignSkipperChange}
|
onChange={onSignSkipperChange}
|
||||||
onPasskeySign={onPasskeySignSkipper}
|
onPasskeySign={onPasskeySignSkipper}
|
||||||
|
onBeforeSign={onBeforeSign}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RoleSignatureBlock
|
<RoleSignatureBlock
|
||||||
@@ -237,6 +250,7 @@ export default function SignatureSection({
|
|||||||
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
|
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
|
||||||
onChange={onSignCrewChange}
|
onChange={onSignCrewChange}
|
||||||
onPasskeySign={onPasskeySignCrew}
|
onPasskeySign={onPasskeySignCrew}
|
||||||
|
onBeforeSign={onBeforeSign}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,6 +131,14 @@
|
|||||||
"sign_classic_or_passkey": "Optional: klassisch unterschreiben oder Passkey-Freigabe oben",
|
"sign_classic_or_passkey": "Optional: klassisch unterschreiben oder Passkey-Freigabe oben",
|
||||||
"sign_crew_passkey_hint": "Crew-Mitglieder mit Schreibzugriff können per Passkey freigeben",
|
"sign_crew_passkey_hint": "Crew-Mitglieder mit Schreibzugriff können per Passkey freigeben",
|
||||||
"sign_offline_hint": "Passkey-Freigabe erfordert Internet — klassische Unterschrift offline möglich",
|
"sign_offline_hint": "Passkey-Freigabe erfordert Internet — klassische Unterschrift offline möglich",
|
||||||
|
"sign_lock_notice": "Nach der Unterschrift sind Änderungen am Logbucheintrag (außer Fotos) nicht möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.",
|
||||||
|
"sign_lock_active": "Dieser Eintrag ist unterschrieben. Änderungen am Logbuch (außer Fotos) entfernen Skipper- und Crew-Unterschrift automatisch.",
|
||||||
|
"sign_lock_warning_title": "Unterschrift bestätigen",
|
||||||
|
"sign_lock_warning": "Nach dem Unterschreiben sind Änderungen am Logbucheintrag (außer Fotos) nicht mehr möglich, ohne dass Skipper und Crew erneut unterschreiben müssen.\n\nMöchten Sie fortfahren?",
|
||||||
|
"sign_proceed": "Unterschreiben",
|
||||||
|
"sign_cancel": "Abbrechen",
|
||||||
|
"sign_cleared_re_sign_title": "Unterschriften entfernt",
|
||||||
|
"sign_cleared_re_sign": "Der Logbucheintrag wurde geändert. Skipper- und Crew-Unterschrift wurden entfernt. Bitte erneut unterschreiben.",
|
||||||
"no_entries": "Keine Logbucheinträge für diese Yacht gefunden. Erstellen Sie Ihren ersten Reisetag!",
|
"no_entries": "Keine Logbucheinträge für diese Yacht gefunden. Erstellen Sie Ihren ersten Reisetag!",
|
||||||
"back_to_list": "Zurück zur Journal-Liste",
|
"back_to_list": "Zurück zur Journal-Liste",
|
||||||
"save": "Logbuchseite speichern",
|
"save": "Logbuchseite speichern",
|
||||||
|
|||||||
@@ -131,6 +131,14 @@
|
|||||||
"sign_classic_or_passkey": "Optional: sign classically below or use Passkey above",
|
"sign_classic_or_passkey": "Optional: sign classically below or use Passkey above",
|
||||||
"sign_crew_passkey_hint": "Write collaborators can sign with their Passkey",
|
"sign_crew_passkey_hint": "Write collaborators can sign with their Passkey",
|
||||||
"sign_offline_hint": "Passkey signing requires internet — classic signature works offline",
|
"sign_offline_hint": "Passkey signing requires internet — classic signature works offline",
|
||||||
|
"sign_lock_notice": "After signing, log entry changes (except photos) require Skipper and Crew to sign again.",
|
||||||
|
"sign_lock_active": "This entry is signed. Changes to the log (except photos) will automatically remove Skipper and Crew signatures.",
|
||||||
|
"sign_lock_warning_title": "Confirm signature",
|
||||||
|
"sign_lock_warning": "After signing, changes to the log entry (except photos) are not possible without Skipper and Crew signing again.\n\nDo you want to proceed?",
|
||||||
|
"sign_proceed": "Sign",
|
||||||
|
"sign_cancel": "Cancel",
|
||||||
|
"sign_cleared_re_sign_title": "Signatures removed",
|
||||||
|
"sign_cleared_re_sign": "The log entry was changed. Skipper and Crew signatures were removed. Please sign again.",
|
||||||
"no_entries": "No logbook entries found for this yacht. Create your first travel day to begin!",
|
"no_entries": "No logbook entries found for this yacht. Create your first travel day to begin!",
|
||||||
"back_to_list": "Back to Journal List",
|
"back_to_list": "Back to Journal List",
|
||||||
"save": "Save Logbook Page",
|
"save": "Save Logbook Page",
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ export function normalizeSignature(value: unknown): SignatureValue | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasAnySignature(
|
||||||
|
skipper: SignatureValue | '' | undefined,
|
||||||
|
crew: SignatureValue | '' | undefined
|
||||||
|
): boolean {
|
||||||
|
return !!(skipper || crew)
|
||||||
|
}
|
||||||
|
|
||||||
export function isSignatureValidForEntry(sig: PasskeySignature, entryHash: string): boolean {
|
export function isSignatureValidForEntry(sig: PasskeySignature, entryHash: string): boolean {
|
||||||
return sig.entryHash === entryHash
|
return sig.entryHash === entryHash
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user