Files
kapteins-daagbok/client/src/components/DeviationForm.tsx
T
elpatron 3cab735754 refactor: replace parseFloat with parseAppDecimal and formatAppDecimal for improved number handling
Updated various components to utilize parseAppDecimal and formatAppDecimal for consistent decimal parsing and formatting. This change enhances the handling of numeric inputs across the application, ensuring better accuracy and user experience in forms and displays.
2026-06-03 18:07:22 +02:00

206 lines
6.4 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { db } from '../services/db.js'
import { getActiveMasterKey } from '../services/auth.js'
import { getLogbookKey } from '../services/logbookKeys.js'
import { encryptJson, decryptJson } from '../services/crypto.js'
import { syncLogbook } from '../services/sync.js'
import { Compass, Save, Check } from 'lucide-react'
import { parseAppDecimalOrZero } from '../utils/numberFormat.js'
interface DeviationFormProps {
logbookId: string
readOnly?: boolean
preloadedData?: any
}
export default function DeviationForm({ logbookId, readOnly = false, preloadedData }: DeviationFormProps) {
const { t } = useTranslation()
// Generate headings: 0, 10, 20, ..., 360 (37 items)
const headings = Array.from({ length: 37 }, (_, i) => i * 10)
// Map representation: heading -> deviation input string (e.g., "1.5", "-2")
const [deviations, setDeviations] = useState<Record<number, string>>({})
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function loadDeviationTable() {
setLoading(true)
setError(null)
try {
if (readOnly && preloadedData) {
const map: Record<number, string> = {}
if (preloadedData.deviations) {
Object.entries(preloadedData.deviations).forEach(([k, v]) => {
map[Number(k)] = String(v)
})
}
setDeviations(map)
return
}
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
const local = await db.deviations.get(logbookId)
if (local) {
const decrypted = await decryptJson(local.encryptedData, local.iv, local.tag, masterKey)
if (decrypted && decrypted.deviations) {
// Map keys back to numbers and set state
const map: Record<number, string> = {}
Object.entries(decrypted.deviations).forEach(([k, v]) => {
map[Number(k)] = String(v)
})
setDeviations(map)
}
} else {
// Initialize empty map
const map: Record<number, string> = {}
headings.forEach((h) => {
map[h] = ''
})
setDeviations(map)
}
} catch (err: any) {
console.error('Failed to load deviation data:', err)
setError(err.message || 'Decryption failed. Could not load deviation table.')
} finally {
setLoading(false)
}
}
loadDeviationTable()
}, [logbookId])
const handleInputChange = (heading: number, val: string) => {
setDeviations((prev) => ({
...prev,
[heading]: val
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (readOnly) return
setSaving(true)
setError(null)
setSuccess(false)
try {
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
// Parse values, substituting 0 if empty
const sanitizedDeviations: Record<number, number> = {}
headings.forEach((h) => {
const val = deviations[h] || ''
const parsed = parseAppDecimalOrZero(val.replace('+', '').trim())
sanitizedDeviations[h] = parsed
})
const dataToSave = {
deviations: sanitizedDeviations
}
// E2E encrypt
const encrypted = await encryptJson(dataToSave, masterKey)
const now = new Date().toISOString()
// Save locally
await db.deviations.put({
logbookId,
encryptedData: encrypted.ciphertext,
iv: encrypted.iv,
tag: encrypted.tag,
updatedAt: now
})
// Queue for background sync
await db.syncQueue.put({
action: 'update',
type: 'deviation',
payloadId: logbookId,
logbookId,
data: JSON.stringify(encrypted),
updatedAt: now
})
setSuccess(true)
setTimeout(() => setSuccess(false), 3000)
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
} catch (err: any) {
console.error('Failed to save deviation table:', err)
setError(err.message || 'Failed to save deviation table.')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div className="tab-placeholder">
<Compass className="header-logo spin" size={48} />
<p>{t('deviation.loading')}</p>
</div>
)
}
return (
<div className="form-card">
<div className="form-header">
<Compass size={24} className="form-icon" />
<div>
<h2>{t('deviation.title')}</h2>
<p className="form-subtitle" style={{ margin: '4px 0 0 0', fontSize: '13px', color: '#94a3b8' }}>
{t('deviation.subtitle')}
</p>
</div>
</div>
{error && <div className="auth-error mb-4">{error}</div>}
<form onSubmit={handleSubmit} className="vessel-form">
<div className="deviation-grid-container mt-6">
{headings.map((h) => {
const paddedLabel = String(h).padStart(3, '0') + '°'
return (
<div key={h} className="deviation-cell">
<span className="cell-label">{paddedLabel}</span>
<input
type="text"
placeholder="0.0"
className="input-text cell-input"
value={deviations[h] || ''}
onChange={(e) => handleInputChange(h, e.target.value)}
disabled={saving || readOnly}
/>
</div>
)
})}
</div>
{!readOnly && (
<div className="form-actions mt-6">
{success && (
<div className="success-toast">
<Check size={16} />
<span>{t('deviation.saved')}</span>
</div>
)}
<button type="submit" className="btn primary" disabled={saving}>
<Save size={18} />
{saving ? t('deviation.saving') : t('deviation.save')}
</button>
</div>
)}
</form>
</div>
)
}