Implement column selector customizer popover for chronological events logbook
This commit is contained in:
@@ -6464,3 +6464,62 @@ body.app-tour-active .feedback-modal-overlay--tour .disclaimer-modal-panel {
|
||||
.member-editor-card .accordion-header:hover {
|
||||
background: var(--app-surface-hover, rgba(255, 255, 255, 0.03));
|
||||
}
|
||||
|
||||
/* Column Selector / Customizer Popover */
|
||||
.column-selector-popover {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
width: 240px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: var(--app-surface-alt, rgba(18, 20, 26, 0.98));
|
||||
border: 1px solid var(--app-border, rgba(255, 255, 255, 0.1));
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.column-selector-title {
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: var(--app-accent, #fbbf24);
|
||||
margin: 0;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--app-border-subtle, rgba(255, 255, 255, 0.06));
|
||||
}
|
||||
|
||||
.column-selector-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.column-selector-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--app-text-muted, #cbd5e1);
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.column-selector-item:hover {
|
||||
background: var(--app-surface-hover, rgba(255, 255, 255, 0.04));
|
||||
color: var(--app-text, #ffffff);
|
||||
}
|
||||
|
||||
.column-selector-item input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--app-accent, #fbbf24);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { syncLogbook } from '../services/sync.js'
|
||||
import { saveEntryDraft, clearEntryDraft } from '../services/entryDraft.js'
|
||||
import { getErrorMessage } from '../utils/errors.js'
|
||||
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
||||
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload, Pencil, X, ChevronDown, ChevronUp, Sparkles } from 'lucide-react'
|
||||
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload, Pencil, X, ChevronDown, ChevronUp, Sparkles, Sliders } from 'lucide-react'
|
||||
import PhotoCapture from './PhotoCapture.tsx'
|
||||
import EventRemarksCell from './EventRemarksCell.tsx'
|
||||
import CreatorAvatar from './CreatorAvatar.tsx'
|
||||
@@ -300,6 +300,65 @@ export default function LogEntryEditor({
|
||||
const [addEventFormCollapsed, setAddEventFormCollapsed] = useState(false)
|
||||
const [tanksCollapsed, setTanksCollapsed] = useState(true)
|
||||
|
||||
const [columnSelectorOpen, setColumnSelectorOpen] = useState(false)
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('logbook_visible_columns')
|
||||
if (saved) {
|
||||
return {
|
||||
mgk: true,
|
||||
rwk: true,
|
||||
windDirection: true,
|
||||
windStrength: true,
|
||||
seaState: true,
|
||||
weather: true,
|
||||
logReading: true,
|
||||
gps: true,
|
||||
...JSON.parse(saved)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing visible columns from localStorage', e)
|
||||
}
|
||||
return {
|
||||
mgk: true,
|
||||
rwk: true,
|
||||
windDirection: true,
|
||||
windStrength: true,
|
||||
seaState: true,
|
||||
weather: true,
|
||||
logReading: true,
|
||||
gps: true,
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('logbook_visible_columns', JSON.stringify(visibleColumns))
|
||||
}, [visibleColumns])
|
||||
|
||||
const columnSelectorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!columnSelectorOpen) return
|
||||
|
||||
const closeOnOutsideClick = (event: MouseEvent) => {
|
||||
if (columnSelectorRef.current && !columnSelectorRef.current.contains(event.target as Node)) {
|
||||
setColumnSelectorOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setColumnSelectorOpen(false)
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', closeOnOutsideClick)
|
||||
document.addEventListener('keydown', closeOnEscape)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', closeOnOutsideClick)
|
||||
document.removeEventListener('keydown', closeOnEscape)
|
||||
}
|
||||
}, [columnSelectorOpen])
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
@@ -1902,6 +1961,99 @@ export default function LogEntryEditor({
|
||||
<div className="accordion-header-title">
|
||||
<Compass size={20} className="form-icon" />
|
||||
<h3>{t('logs.event_title')}</h3>
|
||||
{!eventsCollapsed && (
|
||||
<div
|
||||
ref={columnSelectorRef}
|
||||
className="column-selector-wrapper"
|
||||
style={{ position: 'relative', display: 'inline-block' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setColumnSelectorOpen(!columnSelectorOpen)
|
||||
}}
|
||||
title={t('logs.customize_columns')}
|
||||
style={{ marginLeft: '8px', padding: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<Sliders size={16} />
|
||||
</button>
|
||||
{columnSelectorOpen && (
|
||||
<div className="column-selector-popover" onClick={(e) => e.stopPropagation()}>
|
||||
<h4 className="column-selector-title">{t('logs.column_selector_title')}</h4>
|
||||
<div className="column-selector-list">
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.mgk}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, mgk: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_mgk')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.rwk}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, rwk: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_rwk')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.windDirection}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, windDirection: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_wind_direction')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.windStrength}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, windStrength: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_wind_strength')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.seaState}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, seaState: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_sea_state')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.weather}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, weather: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_weather')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.logReading}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, logReading: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_log')}</span>
|
||||
</label>
|
||||
<label className="column-selector-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns.gps}
|
||||
onChange={(e) => setVisibleColumns(prev => ({ ...prev, gps: e.target.checked }))}
|
||||
/>
|
||||
<span>{t('logs.event_gps')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{eventsCollapsed ? (
|
||||
<ChevronDown size={20} className="accordion-chevron" />
|
||||
@@ -1923,14 +2075,14 @@ export default function LogEntryEditor({
|
||||
<tr>
|
||||
<th>{t('logs.event_time')}</th>
|
||||
<th>{t('logs.event_creator')}</th>
|
||||
<th>{t('logs.event_mgk')}</th>
|
||||
<th>{t('logs.event_rwk')}</th>
|
||||
<th>{t('logs.event_wind_direction')}</th>
|
||||
<th>{t('logs.event_wind_strength')}</th>
|
||||
<th>{t('logs.event_sea_state')}</th>
|
||||
<th>{t('logs.event_weather')}</th>
|
||||
<th>{t('logs.event_log')}</th>
|
||||
<th>{t('logs.event_gps')}</th>
|
||||
{visibleColumns.mgk && <th>{t('logs.event_mgk')}</th>}
|
||||
{visibleColumns.rwk && <th>{t('logs.event_rwk')}</th>}
|
||||
{visibleColumns.windDirection && <th>{t('logs.event_wind_direction')}</th>}
|
||||
{visibleColumns.windStrength && <th>{t('logs.event_wind_strength')}</th>}
|
||||
{visibleColumns.seaState && <th>{t('logs.event_sea_state')}</th>}
|
||||
{visibleColumns.weather && <th>{t('logs.event_weather')}</th>}
|
||||
{visibleColumns.logReading && <th>{t('logs.event_log')}</th>}
|
||||
{visibleColumns.gps && <th>{t('logs.event_gps')}</th>}
|
||||
<th>{t('logs.event_remarks')}</th>
|
||||
{!readOnly && <th></th>}
|
||||
</tr>
|
||||
@@ -1946,27 +2098,31 @@ export default function LogEntryEditor({
|
||||
size={24}
|
||||
/>
|
||||
</td>
|
||||
<td>{ev.mgk ? `${ev.mgk}°` : '—'}</td>
|
||||
<td>{ev.rwk ? `${ev.rwk}°` : '—'}</td>
|
||||
<td>{ev.windDirection || '—'}</td>
|
||||
<td>{ev.windStrength || '—'}</td>
|
||||
<td>{ev.seaState || '—'}</td>
|
||||
<td>
|
||||
{ev.weatherIcon ? (
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${ev.weatherIcon}.png`}
|
||||
alt="Weather"
|
||||
title="Weather Icon"
|
||||
className="table-weather-img"
|
||||
/>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
<td>{ev.logReading ? `${ev.logReading} nm` : '—'}</td>
|
||||
<td className="font-mono text-sm">
|
||||
{ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'}
|
||||
</td>
|
||||
{visibleColumns.mgk && <td>{ev.mgk ? `${ev.mgk}°` : '—'}</td>}
|
||||
{visibleColumns.rwk && <td>{ev.rwk ? `${ev.rwk}°` : '—'}</td>}
|
||||
{visibleColumns.windDirection && <td>{ev.windDirection || '—'}</td>}
|
||||
{visibleColumns.windStrength && <td>{ev.windStrength || '—'}</td>}
|
||||
{visibleColumns.seaState && <td>{ev.seaState || '—'}</td>}
|
||||
{visibleColumns.weather && (
|
||||
<td>
|
||||
{ev.weatherIcon ? (
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${ev.weatherIcon}.png`}
|
||||
alt="Weather"
|
||||
title="Weather Icon"
|
||||
className="table-weather-img"
|
||||
/>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.logReading && <td>{ev.logReading ? `${ev.logReading} nm` : '—'}</td>}
|
||||
{visibleColumns.gps && (
|
||||
<td className="font-mono text-sm">
|
||||
{ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'}
|
||||
</td>
|
||||
)}
|
||||
<td className="remarks-td">
|
||||
<EventRemarksCell
|
||||
event={ev}
|
||||
@@ -2007,12 +2163,20 @@ export default function LogEntryEditor({
|
||||
{/* Mobile view */}
|
||||
<div className="events-mobile-only mb-6">
|
||||
{events.map((ev, idx) => {
|
||||
const hasCourse = ev.mgk || ev.rwk;
|
||||
const hasWind = ev.windDirection || ev.windStrength || ev.windPressure;
|
||||
const hasSeaState = ev.seaState;
|
||||
const hasWeather = ev.weatherIcon;
|
||||
const hasLog = ev.logReading;
|
||||
const hasGps = ev.gpsLat && ev.gpsLng;
|
||||
const displayMgk = visibleColumns.mgk && ev.mgk;
|
||||
const displayRwk = visibleColumns.rwk && ev.rwk;
|
||||
const hasCourse = displayMgk || displayRwk;
|
||||
|
||||
const displayWindDirection = visibleColumns.windDirection && ev.windDirection;
|
||||
const displayWindStrength = visibleColumns.windStrength && ev.windStrength;
|
||||
const displayWindPressure = ev.windPressure;
|
||||
const hasWind = displayWindDirection || displayWindStrength || displayWindPressure;
|
||||
|
||||
const displaySeaState = visibleColumns.seaState && ev.seaState;
|
||||
const displayWeather = visibleColumns.weather && ev.weatherIcon;
|
||||
const displayLog = visibleColumns.logReading && ev.logReading;
|
||||
const displayGps = visibleColumns.gps && ev.gpsLat && ev.gpsLng;
|
||||
|
||||
const hasVisibility = ev.visibility;
|
||||
const hasHeel = ev.heel;
|
||||
const hasSailsOrMotor = ev.sailsOrMotor;
|
||||
@@ -2061,9 +2225,9 @@ export default function LogEntryEditor({
|
||||
<span className="event-card-chip" title={t('logs.event_course_section')}>
|
||||
<Compass size={12} />
|
||||
<span>
|
||||
{ev.mgk ? `MgK: ${ev.mgk}°` : ''}
|
||||
{ev.mgk && ev.rwk ? ' / ' : ''}
|
||||
{ev.rwk ? `rwK: ${ev.rwk}°` : ''}
|
||||
{displayMgk ? `MgK: ${ev.mgk}°` : ''}
|
||||
{displayMgk && displayRwk ? ' / ' : ''}
|
||||
{displayRwk ? `rwK: ${ev.rwk}°` : ''}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
@@ -2073,9 +2237,9 @@ export default function LogEntryEditor({
|
||||
<span>
|
||||
Wind:{' '}
|
||||
{[
|
||||
ev.windDirection,
|
||||
ev.windStrength ? `${ev.windStrength} Bft` : '',
|
||||
ev.windPressure ? `${ev.windPressure} hPa` : ''
|
||||
displayWindDirection ? ev.windDirection : '',
|
||||
displayWindStrength ? `${ev.windStrength} Bft` : '',
|
||||
displayWindPressure ? `${ev.windPressure} hPa` : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ')}
|
||||
@@ -2083,13 +2247,13 @@ export default function LogEntryEditor({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasSeaState && (
|
||||
{displaySeaState && (
|
||||
<span className="event-card-chip" title={t('logs.event_sea_state')}>
|
||||
<span>{t('logs.event_sea_state')}: {ev.seaState}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasWeather && (
|
||||
{displayWeather && (
|
||||
<span className="event-card-chip" title={t('logs.event_weather')}>
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${ev.weatherIcon}.png`}
|
||||
@@ -2099,13 +2263,13 @@ export default function LogEntryEditor({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasLog && (
|
||||
{displayLog && (
|
||||
<span className="event-card-chip" title={t('logs.event_log')}>
|
||||
<span>Log: {ev.logReading} nm</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasGps && (
|
||||
{displayGps && (
|
||||
<span className="event-card-chip" title={t('logs.event_gps')}>
|
||||
<MapPin size={12} />
|
||||
<span className="font-mono text-xs">{ev.gpsLat}, {ev.gpsLng}</span>
|
||||
|
||||
@@ -185,8 +185,10 @@
|
||||
"travel_day_number": "Rejsedag {{number}}",
|
||||
"departure": "Starthavn (rejse fra)",
|
||||
"destination": "Destinationsport (til)",
|
||||
"route": "Rejse fra/til",
|
||||
"route": "Reje fra/til",
|
||||
"tanks": "Tanke",
|
||||
"customize_columns": "Tilpas kolonner",
|
||||
"column_selector_title": "Kolonner at vise",
|
||||
"freshwater": "Ferskvand (liter)",
|
||||
"fuel": "Treibstoff / Brændstof (liter)",
|
||||
"greywater": "Gråt vand (liter)",
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
"destination": "Ziel-Hafen (nach)",
|
||||
"route": "Reise von/nach",
|
||||
"tanks": "Tanks",
|
||||
"customize_columns": "Spalten anpassen",
|
||||
"column_selector_title": "Anzuzeigende Spalten",
|
||||
"freshwater": "Frischwasser (Liter)",
|
||||
"fuel": "Treibstoff / Fuel (Liter)",
|
||||
"greywater": "Grauwasser (Liter)",
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
"destination": "Destination Port (nach)",
|
||||
"route": "Route / Journey",
|
||||
"tanks": "Tanks",
|
||||
"customize_columns": "Customize columns",
|
||||
"column_selector_title": "Columns to Show",
|
||||
"freshwater": "Freshwater (Liters)",
|
||||
"fuel": "Fuel (Liters)",
|
||||
"greywater": "Greywater (Liters)",
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
"destination": "Destinasjonsport (til)",
|
||||
"route": "Reise fra/til",
|
||||
"tanks": "Tanker",
|
||||
"customize_columns": "Tilpass kolonner",
|
||||
"column_selector_title": "Kolonner å vise",
|
||||
"freshwater": "Ferskvann (liter)",
|
||||
"fuel": "Drivstoff / Drivstoff (liter)",
|
||||
"greywater": "Gråvann (liter)",
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
"destination": "Destinationsport (till)",
|
||||
"route": "Resa från/till",
|
||||
"tanks": "Tankar",
|
||||
"customize_columns": "Anpassa kolumner",
|
||||
"column_selector_title": "Kolumner att visa",
|
||||
"freshwater": "Färskvatten (liter)",
|
||||
"fuel": "Treibstoff / Bränsle (liter)",
|
||||
"greywater": "Gråvatten (liter)",
|
||||
|
||||
Reference in New Issue
Block a user