3cab735754
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.
206 lines
6.4 KiB
TypeScript
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>
|
|
)
|
|
}
|