diff --git a/client/src/App.css b/client/src/App.css index 34d346c..f3d1bd8 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -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); +} diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index ad1872b..7989b7e 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -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>(() => { + 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(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({

{t('logs.event_title')}

+ {!eventsCollapsed && ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + + {columnSelectorOpen && ( +
e.stopPropagation()}> +

{t('logs.column_selector_title')}

+
+ + + + + + + + +
+
+ )} +
+ )}
{eventsCollapsed ? ( @@ -1923,14 +2075,14 @@ export default function LogEntryEditor({ {t('logs.event_time')} {t('logs.event_creator')} - {t('logs.event_mgk')} - {t('logs.event_rwk')} - {t('logs.event_wind_direction')} - {t('logs.event_wind_strength')} - {t('logs.event_sea_state')} - {t('logs.event_weather')} - {t('logs.event_log')} - {t('logs.event_gps')} + {visibleColumns.mgk && {t('logs.event_mgk')}} + {visibleColumns.rwk && {t('logs.event_rwk')}} + {visibleColumns.windDirection && {t('logs.event_wind_direction')}} + {visibleColumns.windStrength && {t('logs.event_wind_strength')}} + {visibleColumns.seaState && {t('logs.event_sea_state')}} + {visibleColumns.weather && {t('logs.event_weather')}} + {visibleColumns.logReading && {t('logs.event_log')}} + {visibleColumns.gps && {t('logs.event_gps')}} {t('logs.event_remarks')} {!readOnly && } @@ -1946,27 +2098,31 @@ export default function LogEntryEditor({ size={24} /> - {ev.mgk ? `${ev.mgk}°` : '—'} - {ev.rwk ? `${ev.rwk}°` : '—'} - {ev.windDirection || '—'} - {ev.windStrength || '—'} - {ev.seaState || '—'} - - {ev.weatherIcon ? ( - Weather - ) : ( - '—' - )} - - {ev.logReading ? `${ev.logReading} nm` : '—'} - - {ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'} - + {visibleColumns.mgk && {ev.mgk ? `${ev.mgk}°` : '—'}} + {visibleColumns.rwk && {ev.rwk ? `${ev.rwk}°` : '—'}} + {visibleColumns.windDirection && {ev.windDirection || '—'}} + {visibleColumns.windStrength && {ev.windStrength || '—'}} + {visibleColumns.seaState && {ev.seaState || '—'}} + {visibleColumns.weather && ( + + {ev.weatherIcon ? ( + Weather + ) : ( + '—' + )} + + )} + {visibleColumns.logReading && {ev.logReading ? `${ev.logReading} nm` : '—'}} + {visibleColumns.gps && ( + + {ev.gpsLat && ev.gpsLng ? `${ev.gpsLat}, ${ev.gpsLng}` : '—'} + + )} {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({ - {ev.mgk ? `MgK: ${ev.mgk}°` : ''} - {ev.mgk && ev.rwk ? ' / ' : ''} - {ev.rwk ? `rwK: ${ev.rwk}°` : ''} + {displayMgk ? `MgK: ${ev.mgk}°` : ''} + {displayMgk && displayRwk ? ' / ' : ''} + {displayRwk ? `rwK: ${ev.rwk}°` : ''} )} @@ -2073,9 +2237,9 @@ export default function LogEntryEditor({ 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({ )} - {hasSeaState && ( + {displaySeaState && ( {t('logs.event_sea_state')}: {ev.seaState} )} - {hasWeather && ( + {displayWeather && ( )} - {hasLog && ( + {displayLog && ( Log: {ev.logReading} nm )} - {hasGps && ( + {displayGps && ( {ev.gpsLat}, {ev.gpsLng} diff --git a/client/src/i18n/locales/da.json b/client/src/i18n/locales/da.json index bbbebfa..12d6602 100644 --- a/client/src/i18n/locales/da.json +++ b/client/src/i18n/locales/da.json @@ -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)", diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index 666fec4..ae5e5d9 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -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)", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 0536bd5..0796fe5 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -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)", diff --git a/client/src/i18n/locales/nb.json b/client/src/i18n/locales/nb.json index 5026fa0..afaa202 100644 --- a/client/src/i18n/locales/nb.json +++ b/client/src/i18n/locales/nb.json @@ -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)", diff --git a/client/src/i18n/locales/sv.json b/client/src/i18n/locales/sv.json index 127bbc7..90962d5 100644 --- a/client/src/i18n/locales/sv.json +++ b/client/src/i18n/locales/sv.json @@ -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)",