1842 lines
55 KiB
JavaScript
1842 lines
55 KiB
JavaScript
(function initTradeFairsSubpage() {
|
|
const tableBody = document.getElementById('tradeFairTableBody');
|
|
const searchInput = document.getElementById('tradeFairSearchInput');
|
|
const daysFilterContainer = document.getElementById('tradeFairDaysFilterContainer');
|
|
const daysFilterToggle = document.getElementById('tradeFairDaysFilterToggle');
|
|
const daysFilterInput = document.getElementById('tradeFairDaysFilterInput');
|
|
const meta = document.getElementById('tradeFairMeta');
|
|
const columnSettingsBtn = document.getElementById('tradeFairColumnSettingsBtn');
|
|
const columnsModal = document.getElementById('tradeFairColumnsModal');
|
|
const columnsModalBackdrop = document.getElementById('tradeFairColumnsModalBackdrop');
|
|
const columnsModalClose = document.getElementById('tradeFairColumnsModalClose');
|
|
const columnsModalDone = document.getElementById('tradeFairColumnsDoneBtn');
|
|
const columnsModalReset = document.getElementById('tradeFairColumnsResetBtn');
|
|
const columnsList = document.getElementById('tradeFairColumnsList');
|
|
const sortButtons = Array.from(document.querySelectorAll('.bookmark-subpage__sort[data-trade-sort]'));
|
|
const table = tableBody ? tableBody.closest('table') : null;
|
|
const headerRow = table ? table.querySelector('thead tr') : null;
|
|
const SORT_STATE_KEY = 'fb_trade_fairs_sort_v1';
|
|
const COLUMN_ORDER_STATE_KEY = 'fb_trade_fairs_columns_v1';
|
|
const COLUMN_VISIBILITY_STATE_KEY = 'fb_trade_fairs_column_visibility_v1';
|
|
const LAST_OPEN_STATE_KEY = 'fb_trade_fairs_last_open_v1';
|
|
const DAYS_FILTER_STATE_KEY = 'fb_trade_fairs_days_filter_v1';
|
|
const DEFAULT_COLUMN_ORDER = [
|
|
'tage_bis_start',
|
|
'rang',
|
|
'messe',
|
|
'zuletzt_gesucht_am',
|
|
'thema',
|
|
'stadt',
|
|
'bundesland',
|
|
'termin',
|
|
'besucher',
|
|
'besucher_jahr',
|
|
'besucher_status',
|
|
'ausstellungsflaeche_m2',
|
|
'ticketpreis_we_eur',
|
|
'ticketpreis_unterderwoche_eur',
|
|
'notiz',
|
|
'quelle_homepage'
|
|
];
|
|
|
|
if (!tableBody || !searchInput || !meta || !sortButtons.length || !table || !headerRow) {
|
|
return;
|
|
}
|
|
|
|
const headerCellsByKey = new Map();
|
|
const columnLabelByKey = new Map();
|
|
sortButtons.forEach((button) => {
|
|
const key = button.dataset.tradeSort;
|
|
const th = button.closest('th');
|
|
if (!key || !th) {
|
|
return;
|
|
}
|
|
columnLabelByKey.set(key, button.textContent.trim());
|
|
th.dataset.columnKey = key;
|
|
th.draggable = true;
|
|
headerCellsByKey.set(key, th);
|
|
});
|
|
|
|
if (headerCellsByKey.size !== DEFAULT_COLUMN_ORDER.length) {
|
|
return;
|
|
}
|
|
|
|
const TRADE_FAIRS = [
|
|
{
|
|
rang: 1,
|
|
messe: 'bauma',
|
|
thema: 'Baumaschinen, Baustoffmaschinen, Bergbau, Baufahrzeuge',
|
|
stadt: 'München',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2028-04-03',
|
|
termin_ende: '2028-04-09',
|
|
besucher: 605974,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 420107,
|
|
ticketpreis_we_eur: 38,
|
|
ticketpreis_unterderwoche_eur: 38,
|
|
notiz: 'Besucher laut AUMA. Tagesticket aus zuletzt verfuegbarem Preisstand (TradeFairDates, 2025).',
|
|
quelle_homepage: 'https://bauma.de'
|
|
},
|
|
{
|
|
rang: 2,
|
|
messe: 'IAA MOBILITY',
|
|
thema: 'Mobilitaet, Auto, Tech, Open Space',
|
|
stadt: 'München',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2027-09-08',
|
|
termin_ende: '2027-09-12',
|
|
besucher: 500000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalterangabe 2025 (>500.000)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 39,
|
|
ticketpreis_unterderwoche_eur: 29,
|
|
notiz: 'Open Space teils frei; Ticketpreise laut IAA-Ticketseite (ab EUR 29, Family Pass ab EUR 39).',
|
|
quelle_homepage: 'https://www.iaa-mobility.com'
|
|
},
|
|
{
|
|
rang: 3,
|
|
messe: 'AGRITECHNICA',
|
|
thema: 'Landtechnik und Agrartechnologie',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2027-11-14',
|
|
termin_ende: '2027-11-20',
|
|
besucher: 477055,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 229886,
|
|
ticketpreis_we_eur: 29,
|
|
ticketpreis_unterderwoche_eur: 29,
|
|
notiz: 'Besucher laut AUMA. Ticketpreis laut Agritechnica-Ticketseite (Tagesticket EUR 29).',
|
|
quelle_homepage: 'https://www.agritechnica.com'
|
|
},
|
|
{
|
|
rang: 4,
|
|
messe: 'IdeenExpo',
|
|
thema: 'Technik, Naturwissenschaften, Erlebnis, Ausbildung',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2026-06-20',
|
|
termin_ende: '2026-06-28',
|
|
besucher: 430000,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'Veranstalter/Pressestand 2024',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 0,
|
|
ticketpreis_unterderwoche_eur: 0,
|
|
notiz: 'Eintritt frei (IdeenExpo). Besucherzahl aus dem letzten veroeffentlichten Durchgang.',
|
|
quelle_homepage: 'https://www.ideenexpo.de'
|
|
},
|
|
{
|
|
rang: 5,
|
|
messe: 'gamescom',
|
|
thema: 'Games, digitale Unterhaltung, Hardware/Software',
|
|
stadt: 'Köln',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-08-26',
|
|
termin_ende: '2026-08-30',
|
|
besucher: 357000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalter/Branchenpresse 2025',
|
|
ausstellungsflaeche_m2: 73515,
|
|
ticketpreis_we_eur: 40,
|
|
ticketpreis_unterderwoche_eur: 30.5,
|
|
notiz: 'Wochenende hoeher als Werktag (Preisstand 2025 aus Gaming-/Ticketberichten).',
|
|
quelle_homepage: 'https://www.gamescom.global/en'
|
|
},
|
|
{
|
|
rang: 6,
|
|
messe: 'Grüne Woche',
|
|
thema: 'Ernaehrung, Landwirtschaft, Garten, Genuss',
|
|
stadt: 'Berlin',
|
|
bundesland: 'Berlin',
|
|
termin_start: '2027-01-15',
|
|
termin_ende: '2027-01-24',
|
|
besucher: 307000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalterangabe 2025',
|
|
ausstellungsflaeche_m2: 42500,
|
|
ticketpreis_we_eur: 16,
|
|
ticketpreis_unterderwoche_eur: 16,
|
|
notiz: 'Tagesticket Erwachsene laut Gruene-Woche-Ticketseite (Preisstand 2025).',
|
|
quelle_homepage: 'https://www.gruenewoche.de'
|
|
},
|
|
{
|
|
rang: 7,
|
|
messe: 'CARAVAN SALON DÜSSELDORF',
|
|
thema: 'Reisemobile, Caravans, Camping, Touristik',
|
|
stadt: 'Düsseldorf',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-08-28',
|
|
termin_ende: '2026-09-06',
|
|
besucher: 269000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalter/Branchenpresse 2025',
|
|
ausstellungsflaeche_m2: 121439,
|
|
ticketpreis_we_eur: 20,
|
|
ticketpreis_unterderwoche_eur: 18,
|
|
notiz: 'Preisstand zuletzt verfuegbarer Ticketinfos (Weekend hoeher als Weekday).',
|
|
quelle_homepage: 'https://www.caravan-salon.de'
|
|
},
|
|
{
|
|
rang: 8,
|
|
messe: 'Maimarkt Mannheim',
|
|
thema: 'Publikums-Mehrbranchenmesse',
|
|
stadt: 'Mannheim',
|
|
bundesland: 'Baden-Württemberg',
|
|
termin_start: '2026-04-25',
|
|
termin_ende: '2026-05-05',
|
|
besucher: 268613,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 52769,
|
|
ticketpreis_we_eur: 10,
|
|
ticketpreis_unterderwoche_eur: 10,
|
|
notiz: 'Tagesticket Erwachsene an der Kasse EUR 10 (VVK guenstiger).',
|
|
quelle_homepage: 'https://www.maimarkt.de'
|
|
},
|
|
{
|
|
rang: 9,
|
|
messe: 'CMT - Die Urlaubsmesse',
|
|
thema: 'Tourismus, Caravaning, Freizeit',
|
|
stadt: 'Stuttgart',
|
|
bundesland: 'Baden-Württemberg',
|
|
termin_start: '2027-01-16',
|
|
termin_ende: '2027-01-24',
|
|
besucher: 261004,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 75587,
|
|
ticketpreis_we_eur: 18,
|
|
ticketpreis_unterderwoche_eur: 15,
|
|
notiz: 'Preisstaffel laut CMT-Partner-/Infoseiten (Wochenende teurer).',
|
|
quelle_homepage: 'https://www.messe-stuttgart.de/cmt'
|
|
},
|
|
{
|
|
rang: 10,
|
|
messe: 'Frankfurter Buchmesse',
|
|
thema: 'Buecher, Kultur, Medien, Publishing',
|
|
stadt: 'Frankfurt am Main',
|
|
bundesland: 'Hessen',
|
|
termin_start: '2026-10-07',
|
|
termin_ende: '2026-10-11',
|
|
besucher: 238000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalter/Branchenpresse 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 35,
|
|
ticketpreis_unterderwoche_eur: 30,
|
|
notiz: 'Preisniveau aus zuletzt veroeffentlichtem Ticketstand (Publikumstage teils teurer).',
|
|
quelle_homepage: 'https://www.buchmesse.de'
|
|
},
|
|
{
|
|
rang: 11,
|
|
messe: 'Leipziger Buchmesse / Manga Comic Con',
|
|
bookmark_suchbegriffe: ['Leipziger Buchmesse', 'Manga Comic Con'],
|
|
thema: 'Buchmarkt, Medien, Manga/Comic',
|
|
stadt: 'Leipzig',
|
|
bundesland: 'Sachsen',
|
|
termin_start: '2026-03-19',
|
|
termin_ende: '2026-03-22',
|
|
besucher: 222819,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalter/Pressestand 2025',
|
|
ausstellungsflaeche_m2: 20788,
|
|
ticketpreis_we_eur: 35,
|
|
ticketpreis_unterderwoche_eur: 28,
|
|
notiz: 'Ein Ticket gilt fuer Leipziger Buchmesse und Manga Comic Con (parallel, inkl. Antiquariatsmesse).',
|
|
quelle_homepage: 'https://www.leipziger-buchmesse.de'
|
|
},
|
|
{
|
|
rang: 12,
|
|
messe: 'SPIEL Essen',
|
|
thema: 'Brettspiele, Tabletop, Family Entertainment',
|
|
stadt: 'Essen',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-10-22',
|
|
termin_ende: '2026-10-25',
|
|
besucher: 220000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalterangabe 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 23.5,
|
|
ticketpreis_unterderwoche_eur: 23.5,
|
|
notiz: 'Tagesticket Erwachsene laut Ticketberichten 2025 (einheitlicher Preis).',
|
|
quelle_homepage: 'https://www.spiel-essen.de'
|
|
},
|
|
{
|
|
rang: 13,
|
|
messe: 'ESSEN MOTOR SHOW',
|
|
thema: 'Performance, Tuning, Motorsport, Classic Cars',
|
|
stadt: 'Essen',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-11-28',
|
|
termin_ende: '2026-12-06',
|
|
besucher: 202827,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'AUMA 2024 (letzter verfuegbarer Wert)',
|
|
ausstellungsflaeche_m2: 33781,
|
|
ticketpreis_we_eur: 20,
|
|
ticketpreis_unterderwoche_eur: 20,
|
|
notiz: 'Tagesticket aus zuletzt publiziertem Ticketstand.',
|
|
quelle_homepage: 'https://www.essen-motorshow.de'
|
|
},
|
|
{
|
|
rang: 14,
|
|
messe: 'boot Düsseldorf',
|
|
thema: 'Boote, Wassersport, Tauchen, Yachting',
|
|
stadt: 'Düsseldorf',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2027-01-23',
|
|
termin_ende: '2027-01-31',
|
|
besucher: 198339,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 94867,
|
|
ticketpreis_we_eur: 19,
|
|
ticketpreis_unterderwoche_eur: 19,
|
|
notiz: 'Preis aus offiziellem Ticket-/Club-Stand (2-fuer-1 Aktion auf Basis Tagesticket).',
|
|
quelle_homepage: 'https://www.boot.de'
|
|
},
|
|
{
|
|
rang: 15,
|
|
messe: 'IFA',
|
|
thema: 'Consumer Electronics, Home Appliances, Tech',
|
|
stadt: 'Berlin',
|
|
bundesland: 'Berlin',
|
|
termin_start: '2026-09-04',
|
|
termin_ende: '2026-09-08',
|
|
besucher: 191997,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'AUMA 2024 (letzter verfuegbarer Wert)',
|
|
ausstellungsflaeche_m2: 107546,
|
|
ticketpreis_we_eur: 19.5,
|
|
ticketpreis_unterderwoche_eur: 19.5,
|
|
notiz: 'Preisniveau aus Ticketberichten zum letzten verfuegbaren IFA-Durchgang.',
|
|
quelle_homepage: 'https://www.ifa-berlin.com'
|
|
},
|
|
{
|
|
rang: 16,
|
|
messe: 'FIBO',
|
|
thema: 'Fitness, Wellness, Gesundheit',
|
|
stadt: 'Köln',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-04-16',
|
|
termin_ende: '2026-04-19',
|
|
besucher: 154890,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 56904,
|
|
ticketpreis_we_eur: 44,
|
|
ticketpreis_unterderwoche_eur: 44,
|
|
notiz: 'Tagesticket laut FIBO-Ticketseite (ab EUR 44).',
|
|
quelle_homepage: 'https://www.fibo.com'
|
|
},
|
|
{
|
|
rang: 17,
|
|
messe: 'IAA TRANSPORTATION',
|
|
thema: 'Nutzfahrzeuge, Logistik, Transporttechnologie',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2026-09-15',
|
|
termin_ende: '2026-09-20',
|
|
besucher: 145000,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'Veranstalterangabe 2024',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 20,
|
|
ticketpreis_unterderwoche_eur: 20,
|
|
notiz: 'Ticketpreis laut offizieller IAA-Transportation-Ticketseite (ab EUR 20).',
|
|
quelle_homepage: 'https://www.iaa-transportation.com'
|
|
},
|
|
{
|
|
rang: 18,
|
|
messe: 'Anuga',
|
|
thema: 'Lebensmittel und Getraenke',
|
|
stadt: 'Köln',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2027-10-09',
|
|
termin_ende: '2027-10-13',
|
|
besucher: 143432,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 157545,
|
|
ticketpreis_we_eur: 80,
|
|
ticketpreis_unterderwoche_eur: 80,
|
|
notiz: 'Tagesticket laut TradeFairDates (Stand zuletzt verfuegbarer Preis).',
|
|
quelle_homepage: 'https://www.anuga.com'
|
|
},
|
|
{
|
|
rang: 19,
|
|
messe: 'HANNOVER MESSE',
|
|
thema: 'Industrie, Automation, Energie, Digital',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2026-04-20',
|
|
termin_ende: '2026-04-24',
|
|
besucher: 123035,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 101699,
|
|
ticketpreis_we_eur: null,
|
|
ticketpreis_unterderwoche_eur: 39,
|
|
notiz: 'Messe liegt auf Werktagen; Tagesticket laut TradeFairDates EUR 39.',
|
|
quelle_homepage: 'https://www.hannovermesse.de'
|
|
},
|
|
{
|
|
rang: 20,
|
|
messe: 'CONSUMENTA',
|
|
thema: 'Publikumsmesse: Haushalt, Freizeit, Bauen, Genuss',
|
|
stadt: 'Nürnberg',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2026-10-31',
|
|
termin_ende: '2026-11-08',
|
|
besucher: 123000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: 30448,
|
|
ticketpreis_we_eur: 16,
|
|
ticketpreis_unterderwoche_eur: 16,
|
|
notiz: 'Aktionstags-Preisstand vorhanden; regulaere Ticketpreise fuer 2026 noch im Rollout.',
|
|
quelle_homepage: 'https://www.consumenta.de'
|
|
},
|
|
{
|
|
rang: 21,
|
|
messe: 'f.re.e München / IMOT',
|
|
bookmark_suchbegriffe: ['f.re.e München', 'IMOT'],
|
|
thema: 'Reisen, Freizeit, Caravaning, Outdoor',
|
|
stadt: 'München',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2026-02-18',
|
|
termin_ende: '2026-02-22',
|
|
besucher: 120000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 17,
|
|
ticketpreis_unterderwoche_eur: 14,
|
|
notiz: 'Ein Ticket gilt fuer f.re.e und IMOT (sowie meist auch Muenchner Auto Tage) laut offiziellem Ticketshop.',
|
|
quelle_homepage: 'https://free-muenchen.de'
|
|
},
|
|
{
|
|
rang: 22,
|
|
messe: 'infa',
|
|
thema: 'Publikumsmesse: Lifestyle, Haushalt, Genuss',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2026-10-10',
|
|
termin_ende: '2026-10-18',
|
|
besucher: 112228,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: 16423,
|
|
ticketpreis_we_eur: 15,
|
|
ticketpreis_unterderwoche_eur: 15,
|
|
notiz: 'Tagesticket laut zuletzt verfuegbarem Preisstand.',
|
|
quelle_homepage: 'https://www.infa.de'
|
|
},
|
|
{
|
|
rang: 23,
|
|
messe: 'ITB Berlin',
|
|
thema: 'Reiseindustrie (B2B/Fachbesucher)',
|
|
stadt: 'Berlin',
|
|
bundesland: 'Berlin',
|
|
termin_start: '2026-03-03',
|
|
termin_ende: '2026-03-05',
|
|
besucher: 100000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: 78892,
|
|
ticketpreis_we_eur: null,
|
|
ticketpreis_unterderwoche_eur: 65,
|
|
notiz: 'B2B-Fachmesse ohne Wochenend-Tag. Ticketpreis laut offizieller ITB-Ticketseite.',
|
|
quelle_homepage: 'https://www.itb.com'
|
|
},
|
|
{
|
|
rang: 24,
|
|
messe: 'ILA Berlin',
|
|
thema: 'Luft- und Raumfahrt',
|
|
stadt: 'Berlin',
|
|
bundesland: 'Berlin',
|
|
termin_start: '2026-06-10',
|
|
termin_ende: '2026-06-14',
|
|
besucher: 95000,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'AUMA 2024 (letzter verfuegbarer Wert)',
|
|
ausstellungsflaeche_m2: 32008,
|
|
ticketpreis_we_eur: 22,
|
|
ticketpreis_unterderwoche_eur: 22,
|
|
notiz: 'Tagesticket laut ILA-Besucherinfo (EUR 22, Kinder frei).',
|
|
quelle_homepage: 'https://www.ila-berlin.de'
|
|
},
|
|
{
|
|
rang: 25,
|
|
messe: 'Heim+Handwerk / FOOD & LIFE',
|
|
bookmark_suchbegriffe: ['Heim+Handwerk', 'FOOD & LIFE'],
|
|
thema: 'Wohnen, Handwerk, Lifestyle',
|
|
stadt: 'München',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2026-11-25',
|
|
termin_ende: '2026-11-29',
|
|
besucher: 90000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 15,
|
|
ticketpreis_unterderwoche_eur: 15,
|
|
notiz: 'Ein Ticket gilt fuer Heim+Handwerk und FOOD & LIFE (parallel in Muenchen).',
|
|
quelle_homepage: 'https://www.heim-handwerk.de'
|
|
},
|
|
{
|
|
rang: 26,
|
|
messe: 'offerta',
|
|
thema: 'Einkaufs- und Erlebnismesse fuer Verbraucher',
|
|
stadt: 'Karlsruhe',
|
|
bundesland: 'Baden-Württemberg',
|
|
termin_start: '2026-10-24',
|
|
termin_ende: '2026-11-01',
|
|
besucher: 88738,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 16242,
|
|
ticketpreis_we_eur: 10,
|
|
ticketpreis_unterderwoche_eur: 10,
|
|
notiz: 'Preisniveau aus regional veroeffentlichtem Ticketstand (offerta Karlsruhe).',
|
|
quelle_homepage: 'https://www.offerta.info'
|
|
},
|
|
{
|
|
rang: 27,
|
|
messe: 'Reise + Camping Essen / Fahrrad Essen',
|
|
bookmark_suchbegriffe: ['Reise + Camping Essen', 'Fahrrad Essen'],
|
|
thema: 'Reisen, Camping, Fahrrad, Outdoor',
|
|
stadt: 'Essen',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-02-25',
|
|
termin_ende: '2026-03-01',
|
|
besucher: 80000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 12,
|
|
ticketpreis_unterderwoche_eur: 12,
|
|
notiz: 'Ein Ticket gilt fuer Reise + Camping und Fahrrad Essen (parallel in den Messehallen).',
|
|
quelle_homepage: 'https://www.reise-camping.de'
|
|
},
|
|
{
|
|
rang: 28,
|
|
messe: 'RETRO CLASSICS',
|
|
thema: 'Klassische Fahrzeuge und Fahrkultur',
|
|
stadt: 'Stuttgart',
|
|
bundesland: 'Baden-Württemberg',
|
|
termin_start: '2026-02-19',
|
|
termin_ende: '2026-02-22',
|
|
besucher: 72386,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'AUMA 2025',
|
|
ausstellungsflaeche_m2: 46496,
|
|
ticketpreis_we_eur: 25,
|
|
ticketpreis_unterderwoche_eur: 25,
|
|
notiz: 'Tagesticket laut offizieller Retro-Classics-Preisliste.',
|
|
quelle_homepage: 'https://www.retro-classics.de'
|
|
},
|
|
{
|
|
rang: 29,
|
|
messe: 'Freizeit Messe Nürnberg',
|
|
thema: 'Freizeit, Touristik, Garten',
|
|
stadt: 'Nürnberg',
|
|
bundesland: 'Bayern',
|
|
termin_start: '2026-03-04',
|
|
termin_ende: '2026-03-08',
|
|
besucher: 71000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 16,
|
|
ticketpreis_unterderwoche_eur: 15,
|
|
notiz: 'Ticketpreise laut offizieller Freizeit-Messe-Preistabelle 2026.',
|
|
quelle_homepage: 'https://www.freizeitmesse.de'
|
|
},
|
|
{
|
|
rang: 30,
|
|
messe: 'Thüringen Ausstellung',
|
|
thema: 'Haus, Bau, Modernisieren, Lifestyle',
|
|
stadt: 'Erfurt',
|
|
bundesland: 'Thüringen',
|
|
termin_start: '2026-02-28',
|
|
termin_ende: '2026-03-08',
|
|
besucher: 69988,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 14,
|
|
ticketpreis_unterderwoche_eur: 14,
|
|
notiz: 'Tagesticket Erwachsene laut offizieller Thueringen-Ausstellung-Preisliste.',
|
|
quelle_homepage: 'https://thueringen-ausstellung.de'
|
|
},
|
|
{
|
|
rang: 31,
|
|
messe: 'CREATIVA Dortmund',
|
|
thema: 'Kreativmesse, DIY, Basteln, Handarbeit',
|
|
stadt: 'Dortmund',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2026-03-25',
|
|
termin_ende: '2026-03-29',
|
|
besucher: 65000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veranstalterangabe 2025 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 17,
|
|
ticketpreis_unterderwoche_eur: 17,
|
|
notiz: 'Tagesticket Erwachsene EUR 17 (erm. EUR 10) laut Veranstaltungsseite Dortmund.',
|
|
quelle_homepage: 'https://www.messe-creativa.de'
|
|
},
|
|
{
|
|
rang: 32,
|
|
messe: 'didacta Köln',
|
|
thema: 'Bildungsmesse, Schule, Kita, Weiterbildung',
|
|
stadt: 'Köln',
|
|
bundesland: 'Nordrhein-Westfalen',
|
|
termin_start: '2027-02-23',
|
|
termin_ende: '2027-02-27',
|
|
besucher: 63000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 20,
|
|
ticketpreis_unterderwoche_eur: 20,
|
|
notiz: 'Tagesticket Erwachsene laut zuletzt verfuegbarem didacta-Preisstand.',
|
|
quelle_homepage: 'https://www.didacta-koeln.de'
|
|
},
|
|
{
|
|
rang: 33,
|
|
messe: 'INTERNORGA',
|
|
thema: 'Hotellerie, Gastronomie, Baeckerei, Konditorei',
|
|
stadt: 'Hamburg',
|
|
bundesland: 'Hamburg',
|
|
termin_start: '2026-03-13',
|
|
termin_ende: '2026-03-17',
|
|
besucher: 59000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 45,
|
|
ticketpreis_unterderwoche_eur: 45,
|
|
notiz: 'Fachmesse; Tagesticket laut zuletzt verfuegbarem INTERNORGA-Preisstand.',
|
|
quelle_homepage: 'https://www.internorga.com'
|
|
},
|
|
{
|
|
rang: 34,
|
|
messe: 'PFERD & JAGD',
|
|
thema: 'Pferdesport, Jagd, Outdoor, Hund',
|
|
stadt: 'Hannover',
|
|
bundesland: 'Niedersachsen',
|
|
termin_start: '2026-12-03',
|
|
termin_ende: '2026-12-06',
|
|
besucher: 55000,
|
|
besucher_jahr: 2024,
|
|
besucher_status: 'Veroeffentlichter Wert 2024 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 19,
|
|
ticketpreis_unterderwoche_eur: 19,
|
|
notiz: 'Tagesticket Erwachsene laut zuletzt veroeffentlichter Preisliste.',
|
|
quelle_homepage: 'https://www.pferd-und-jagd-messe.de'
|
|
},
|
|
{
|
|
rang: 35,
|
|
messe: 'HAUS-GARTEN-FREIZEIT',
|
|
thema: 'Bauen, Wohnen, Garten, Freizeit',
|
|
stadt: 'Leipzig',
|
|
bundesland: 'Sachsen',
|
|
termin_start: '2026-02-14',
|
|
termin_ende: '2026-02-22',
|
|
besucher: 52000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 15,
|
|
ticketpreis_unterderwoche_eur: 14,
|
|
notiz: 'Tagesticket laut zuletzt verfuegbarem Preisstand (Wochenende leicht hoeher).',
|
|
quelle_homepage: 'https://www.haus-garten-freizeit.de'
|
|
},
|
|
{
|
|
rang: 36,
|
|
messe: 'REISEN & CARAVANING Hamburg',
|
|
thema: 'Reisen, Caravaning, Kreuzfahrt, Outdoor',
|
|
stadt: 'Hamburg',
|
|
bundesland: 'Hamburg',
|
|
termin_start: '2026-02-04',
|
|
termin_ende: '2026-02-08',
|
|
besucher: 50000,
|
|
besucher_jahr: 2025,
|
|
besucher_status: 'Veroeffentlichter Wert 2025 (rund)',
|
|
ausstellungsflaeche_m2: null,
|
|
ticketpreis_we_eur: 12,
|
|
ticketpreis_unterderwoche_eur: 12,
|
|
notiz: 'Tagesticket Erwachsene laut zuletzt veroeffentlichtem Preisstand.',
|
|
quelle_homepage: 'https://www.reisenhamburg.de'
|
|
}
|
|
];
|
|
|
|
function isFreeTradeFair(row) {
|
|
const prices = [
|
|
toNumber(row.ticketpreis_we_eur),
|
|
toNumber(row.ticketpreis_unterderwoche_eur)
|
|
].filter((value) => value !== null);
|
|
|
|
if (!prices.length) {
|
|
return false;
|
|
}
|
|
|
|
return prices.every((price) => price === 0);
|
|
}
|
|
|
|
const EFFECTIVE_TRADE_FAIRS = TRADE_FAIRS
|
|
.filter((row) => !isFreeTradeFair(row))
|
|
.sort((a, b) => (a.rang || 0) - (b.rang || 0))
|
|
.map((row, index) => ({
|
|
...row,
|
|
rang: index + 1
|
|
}));
|
|
|
|
const SORT_TYPES = {
|
|
tage_bis_start: 'number',
|
|
rang: 'number',
|
|
messe: 'text',
|
|
zuletzt_gesucht_am: 'date',
|
|
thema: 'text',
|
|
stadt: 'text',
|
|
bundesland: 'text',
|
|
termin: 'date',
|
|
besucher: 'number',
|
|
besucher_jahr: 'number',
|
|
besucher_status: 'text',
|
|
ausstellungsflaeche_m2: 'number',
|
|
ticketpreis_we_eur: 'number',
|
|
ticketpreis_unterderwoche_eur: 'number',
|
|
notiz: 'text',
|
|
quelle_homepage: 'text'
|
|
};
|
|
|
|
let sortKey = 'rang';
|
|
let sortDirection = 'asc';
|
|
let searchTerm = '';
|
|
let daysFilterTerm = '';
|
|
let isDaysFilterOpen = false;
|
|
let draggedColumnKey = null;
|
|
let lastOpenedByTradeFair = {};
|
|
let columnVisibility = Object.fromEntries(DEFAULT_COLUMN_ORDER.map((key) => [key, true]));
|
|
|
|
function toNumber(value) {
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === 'string' && value.trim()) {
|
|
const normalized = value.replace(/\./g, '').replace(',', '.').replace(/[^\d.-]/g, '');
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function toDate(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
return null;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function getDaysUntil(startIso) {
|
|
const start = toDate(startIso);
|
|
if (!start) {
|
|
return null;
|
|
}
|
|
const now = new Date();
|
|
now.setHours(0, 0, 0, 0);
|
|
start.setHours(0, 0, 0, 0);
|
|
return Math.round((start.getTime() - now.getTime()) / 86400000);
|
|
}
|
|
|
|
function normalizeRow(row) {
|
|
return {
|
|
...row,
|
|
tage_bis_start: getDaysUntil(row.termin_start),
|
|
zuletzt_gesucht_am: lastOpenedByTradeFair[getTradeFairOpenKey(row)] || null,
|
|
termin: row.termin_start || null
|
|
};
|
|
}
|
|
|
|
function getSortValue(row, key) {
|
|
const type = SORT_TYPES[key] || 'text';
|
|
const value = row[key];
|
|
|
|
if (type === 'number') {
|
|
const numeric = toNumber(value);
|
|
return numeric === null ? Number.NEGATIVE_INFINITY : numeric;
|
|
}
|
|
|
|
if (type === 'date') {
|
|
const date = toDate(value);
|
|
return date ? date.getTime() : Number.NEGATIVE_INFINITY;
|
|
}
|
|
|
|
return String(value || '').toLocaleLowerCase('de-DE');
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (!Number.isFinite(value)) {
|
|
return 'k.A.';
|
|
}
|
|
return value.toLocaleString('de-DE');
|
|
}
|
|
|
|
function formatPrice(value) {
|
|
if (!Number.isFinite(value)) {
|
|
return 'k.A.';
|
|
}
|
|
return value.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
const date = toDate(iso);
|
|
if (!date) {
|
|
return 'k.A.';
|
|
}
|
|
return date.toLocaleDateString('de-DE');
|
|
}
|
|
|
|
function formatTermRange(startIso, endIso) {
|
|
const start = toDate(startIso);
|
|
const end = toDate(endIso);
|
|
|
|
if (!start && !end) {
|
|
return 'k.A.';
|
|
}
|
|
if (start && !end) {
|
|
return formatDate(startIso);
|
|
}
|
|
if (!start && end) {
|
|
return formatDate(endIso);
|
|
}
|
|
|
|
const startDay = start.getDate();
|
|
const startMonth = start.getMonth() + 1;
|
|
const startYear = start.getFullYear();
|
|
const endDay = end.getDate();
|
|
const endMonth = end.getMonth() + 1;
|
|
const endYear = end.getFullYear();
|
|
|
|
if (startYear === endYear) {
|
|
if (startMonth === endMonth) {
|
|
if (startDay === endDay) {
|
|
return `${startDay}.${startMonth}.${startYear}`;
|
|
}
|
|
return `${startDay}.-${endDay}.${startMonth}.${startYear}`;
|
|
}
|
|
return `${startDay}.${startMonth}.-${endDay}.${endMonth}.${startYear}`;
|
|
}
|
|
|
|
return `${startDay}.${startMonth}.${startYear}-${endDay}.${endMonth}.${endYear}`;
|
|
}
|
|
|
|
function matchesSearch(row, term) {
|
|
if (!term) {
|
|
return true;
|
|
}
|
|
const haystack = [
|
|
row.messe,
|
|
row.thema,
|
|
row.stadt,
|
|
row.bundesland,
|
|
row.notiz
|
|
].join(' ').toLocaleLowerCase('de-DE');
|
|
return haystack.includes(term);
|
|
}
|
|
|
|
function parseDaysFilter(term) {
|
|
const normalized = String(term || '').trim();
|
|
if (!normalized) {
|
|
return { type: 'all' };
|
|
}
|
|
|
|
const comparisonMatch = normalized.match(/^(<=|>=|!=|=|<|>)\s*(-?\d+)$/);
|
|
if (comparisonMatch) {
|
|
return {
|
|
type: 'comparison',
|
|
operator: comparisonMatch[1],
|
|
value: Number(comparisonMatch[2])
|
|
};
|
|
}
|
|
|
|
const rangeMatch = normalized.match(/^(-?\d+)\s*-\s*(-?\d+)$/);
|
|
if (rangeMatch) {
|
|
const first = Number(rangeMatch[1]);
|
|
const second = Number(rangeMatch[2]);
|
|
return {
|
|
type: 'range',
|
|
min: Math.min(first, second),
|
|
max: Math.max(first, second)
|
|
};
|
|
}
|
|
|
|
const exactMatch = normalized.match(/^-?\d+$/);
|
|
if (exactMatch) {
|
|
return {
|
|
type: 'comparison',
|
|
operator: '=',
|
|
value: Number(normalized)
|
|
};
|
|
}
|
|
|
|
return { type: 'invalid', raw: normalized };
|
|
}
|
|
|
|
function matchesDaysFilter(value, parsedFilter) {
|
|
if (!parsedFilter || parsedFilter.type === 'all' || parsedFilter.type === 'invalid') {
|
|
return true;
|
|
}
|
|
|
|
const numeric = toNumber(value);
|
|
if (numeric === null) {
|
|
return false;
|
|
}
|
|
|
|
if (parsedFilter.type === 'range') {
|
|
return numeric >= parsedFilter.min && numeric <= parsedFilter.max;
|
|
}
|
|
|
|
if (parsedFilter.type === 'comparison') {
|
|
switch (parsedFilter.operator) {
|
|
case '>':
|
|
return numeric > parsedFilter.value;
|
|
case '>=':
|
|
return numeric >= parsedFilter.value;
|
|
case '<':
|
|
return numeric < parsedFilter.value;
|
|
case '<=':
|
|
return numeric <= parsedFilter.value;
|
|
case '!=':
|
|
return numeric !== parsedFilter.value;
|
|
case '=':
|
|
default:
|
|
return numeric === parsedFilter.value;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getDaysFilterMetaLabel(parsedFilter) {
|
|
if (!parsedFilter || parsedFilter.type === 'all') {
|
|
return '';
|
|
}
|
|
|
|
if (parsedFilter.type === 'invalid') {
|
|
return `Tage-bis-Start-Filter ungültig (${parsedFilter.raw})`;
|
|
}
|
|
|
|
if (parsedFilter.type === 'range') {
|
|
return `Tage-bis-Start-Filter ${parsedFilter.min}-${parsedFilter.max}`;
|
|
}
|
|
|
|
return `Tage-bis-Start-Filter ${parsedFilter.operator}${parsedFilter.value}`;
|
|
}
|
|
|
|
function sortRows(rows) {
|
|
return [...rows].sort((a, b) => {
|
|
const aValue = getSortValue(a, sortKey);
|
|
const bValue = getSortValue(b, sortKey);
|
|
|
|
if (aValue === bValue) {
|
|
const fallback = (a.rang || 0) - (b.rang || 0);
|
|
return sortDirection === 'asc' ? fallback : -fallback;
|
|
}
|
|
|
|
if (aValue > bValue) {
|
|
return sortDirection === 'asc' ? 1 : -1;
|
|
}
|
|
return sortDirection === 'asc' ? -1 : 1;
|
|
});
|
|
}
|
|
|
|
function normalizeSortState(rawState) {
|
|
if (!rawState || typeof rawState !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
const key = rawState.sortKey;
|
|
const direction = rawState.sortDirection;
|
|
if (!Object.prototype.hasOwnProperty.call(SORT_TYPES, key)) {
|
|
return null;
|
|
}
|
|
if (direction !== 'asc' && direction !== 'desc') {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
sortKey: key,
|
|
sortDirection: direction
|
|
};
|
|
}
|
|
|
|
function normalizeColumnOrder(rawOrder) {
|
|
if (!Array.isArray(rawOrder) || rawOrder.length !== DEFAULT_COLUMN_ORDER.length) {
|
|
return null;
|
|
}
|
|
|
|
const filtered = rawOrder.filter((key) => DEFAULT_COLUMN_ORDER.includes(key));
|
|
if (filtered.length !== DEFAULT_COLUMN_ORDER.length) {
|
|
return null;
|
|
}
|
|
if (new Set(filtered).size !== DEFAULT_COLUMN_ORDER.length) {
|
|
return null;
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
function getColumnOrderFromHeader() {
|
|
const keys = Array.from(headerRow.children)
|
|
.map((th) => th.dataset.columnKey)
|
|
.filter((key) => DEFAULT_COLUMN_ORDER.includes(key));
|
|
|
|
if (keys.length !== DEFAULT_COLUMN_ORDER.length) {
|
|
return [...DEFAULT_COLUMN_ORDER];
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
function applyColumnOrder(order) {
|
|
order.forEach((key) => {
|
|
const th = headerCellsByKey.get(key);
|
|
if (th) {
|
|
headerRow.appendChild(th);
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadColumnOrder() {
|
|
try {
|
|
const raw = localStorage.getItem(COLUMN_ORDER_STATE_KEY);
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const normalized = normalizeColumnOrder(parsed);
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
applyColumnOrder(normalized);
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function persistColumnOrder() {
|
|
try {
|
|
localStorage.setItem(COLUMN_ORDER_STATE_KEY, JSON.stringify(getColumnOrderFromHeader()));
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function normalizeColumnVisibility(rawState) {
|
|
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
|
|
return null;
|
|
}
|
|
|
|
const normalized = {};
|
|
DEFAULT_COLUMN_ORDER.forEach((key) => {
|
|
const value = rawState[key];
|
|
normalized[key] = value !== false;
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
function loadColumnVisibility() {
|
|
try {
|
|
const raw = localStorage.getItem(COLUMN_VISIBILITY_STATE_KEY);
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const normalized = normalizeColumnVisibility(parsed);
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
columnVisibility = normalized;
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function persistColumnVisibility() {
|
|
try {
|
|
localStorage.setItem(COLUMN_VISIBILITY_STATE_KEY, JSON.stringify(columnVisibility));
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function isColumnVisible(columnKey) {
|
|
return columnVisibility[columnKey] !== false;
|
|
}
|
|
|
|
function getVisibleColumnOrder() {
|
|
const order = getColumnOrderFromHeader().filter((columnKey) => isColumnVisible(columnKey));
|
|
if (order.length) {
|
|
return order;
|
|
}
|
|
return ['messe'];
|
|
}
|
|
|
|
function ensureValidSortColumn() {
|
|
if (isColumnVisible(sortKey)) {
|
|
return;
|
|
}
|
|
const visibleColumns = getVisibleColumnOrder();
|
|
if (visibleColumns.includes('rang')) {
|
|
sortKey = 'rang';
|
|
sortDirection = 'asc';
|
|
persistSortState();
|
|
return;
|
|
}
|
|
sortKey = visibleColumns[0] || 'messe';
|
|
sortDirection = 'asc';
|
|
persistSortState();
|
|
}
|
|
|
|
function applyColumnVisibilityToHeader() {
|
|
headerCellsByKey.forEach((th, key) => {
|
|
const visible = isColumnVisible(key);
|
|
th.hidden = !visible;
|
|
th.style.display = visible ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
function getColumnLabel(columnKey) {
|
|
const fromBase = sortButtons.find((button) => button.dataset.tradeSort === columnKey)?.dataset.baseLabel;
|
|
if (fromBase) {
|
|
return fromBase;
|
|
}
|
|
return columnLabelByKey.get(columnKey) || columnKey;
|
|
}
|
|
|
|
function moveColumnInOrder(columnKey, direction) {
|
|
const order = getColumnOrderFromHeader();
|
|
const index = order.indexOf(columnKey);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
const targetIndex = direction < 0 ? index - 1 : index + 1;
|
|
if (targetIndex < 0 || targetIndex >= order.length) {
|
|
return;
|
|
}
|
|
|
|
const nextOrder = [...order];
|
|
const [item] = nextOrder.splice(index, 1);
|
|
nextOrder.splice(targetIndex, 0, item);
|
|
applyColumnOrder(nextOrder);
|
|
persistColumnOrder();
|
|
applyColumnVisibilityToHeader();
|
|
render();
|
|
renderColumnSettingsList();
|
|
}
|
|
|
|
function setColumnVisibility(columnKey, visible) {
|
|
const nextState = {
|
|
...columnVisibility,
|
|
[columnKey]: visible
|
|
};
|
|
const visibleCount = DEFAULT_COLUMN_ORDER.filter((key) => nextState[key] !== false).length;
|
|
if (visibleCount === 0) {
|
|
return false;
|
|
}
|
|
|
|
columnVisibility = nextState;
|
|
persistColumnVisibility();
|
|
applyColumnVisibilityToHeader();
|
|
ensureValidSortColumn();
|
|
updateSortButtonState();
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function renderColumnSettingsList() {
|
|
if (!columnsList) {
|
|
return;
|
|
}
|
|
|
|
const order = getColumnOrderFromHeader();
|
|
columnsList.innerHTML = '';
|
|
order.forEach((columnKey, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'bookmark-columns-modal__item';
|
|
|
|
const label = document.createElement('label');
|
|
label.className = 'bookmark-columns-modal__toggle';
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = isColumnVisible(columnKey);
|
|
checkbox.addEventListener('change', () => {
|
|
const ok = setColumnVisibility(columnKey, checkbox.checked);
|
|
if (!ok) {
|
|
checkbox.checked = true;
|
|
return;
|
|
}
|
|
renderColumnSettingsList();
|
|
});
|
|
const text = document.createElement('span');
|
|
text.textContent = getColumnLabel(columnKey);
|
|
label.append(checkbox, text);
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'bookmark-columns-modal__item-actions';
|
|
const upButton = document.createElement('button');
|
|
upButton.type = 'button';
|
|
upButton.className = 'bookmark-columns-modal__move';
|
|
upButton.textContent = '↑';
|
|
upButton.title = 'Nach oben';
|
|
upButton.disabled = index === 0;
|
|
upButton.addEventListener('click', () => {
|
|
moveColumnInOrder(columnKey, -1);
|
|
});
|
|
|
|
const downButton = document.createElement('button');
|
|
downButton.type = 'button';
|
|
downButton.className = 'bookmark-columns-modal__move';
|
|
downButton.textContent = '↓';
|
|
downButton.title = 'Nach unten';
|
|
downButton.disabled = index === order.length - 1;
|
|
downButton.addEventListener('click', () => {
|
|
moveColumnInOrder(columnKey, 1);
|
|
});
|
|
|
|
actions.append(upButton, downButton);
|
|
item.append(label, actions);
|
|
columnsList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function closeColumnSettingsModal() {
|
|
if (!columnsModal) {
|
|
return;
|
|
}
|
|
columnsModal.hidden = true;
|
|
}
|
|
|
|
function openColumnSettingsModal() {
|
|
if (!columnsModal) {
|
|
return;
|
|
}
|
|
renderColumnSettingsList();
|
|
columnsModal.hidden = false;
|
|
}
|
|
|
|
function resetColumnSettings() {
|
|
applyColumnOrder(DEFAULT_COLUMN_ORDER);
|
|
columnVisibility = Object.fromEntries(DEFAULT_COLUMN_ORDER.map((key) => [key, true]));
|
|
persistColumnOrder();
|
|
persistColumnVisibility();
|
|
applyColumnVisibilityToHeader();
|
|
ensureValidSortColumn();
|
|
updateSortButtonState();
|
|
render();
|
|
renderColumnSettingsList();
|
|
}
|
|
|
|
function setupColumnSettingsModal() {
|
|
if (!columnSettingsBtn || !columnsModal) {
|
|
return;
|
|
}
|
|
|
|
columnSettingsBtn.addEventListener('click', () => {
|
|
openColumnSettingsModal();
|
|
});
|
|
|
|
if (columnsModalBackdrop) {
|
|
columnsModalBackdrop.addEventListener('click', () => {
|
|
closeColumnSettingsModal();
|
|
});
|
|
}
|
|
|
|
if (columnsModalClose) {
|
|
columnsModalClose.addEventListener('click', () => {
|
|
closeColumnSettingsModal();
|
|
});
|
|
}
|
|
|
|
if (columnsModalDone) {
|
|
columnsModalDone.addEventListener('click', () => {
|
|
closeColumnSettingsModal();
|
|
});
|
|
}
|
|
|
|
if (columnsModalReset) {
|
|
columnsModalReset.addEventListener('click', () => {
|
|
resetColumnSettings();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape' && columnsModal && !columnsModal.hidden) {
|
|
closeColumnSettingsModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
function normalizeLastOpenedState(rawState) {
|
|
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
|
|
return {};
|
|
}
|
|
|
|
const normalized = {};
|
|
Object.entries(rawState).forEach(([key, value]) => {
|
|
if (typeof key !== 'string' || typeof value !== 'string') {
|
|
return;
|
|
}
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
return;
|
|
}
|
|
normalized[key] = parsed.toISOString();
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
function loadLastOpenedState() {
|
|
try {
|
|
const raw = localStorage.getItem(LAST_OPEN_STATE_KEY);
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
return normalizeLastOpenedState(JSON.parse(raw));
|
|
} catch (_error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function persistLastOpenedState() {
|
|
try {
|
|
localStorage.setItem(LAST_OPEN_STATE_KEY, JSON.stringify(lastOpenedByTradeFair));
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function getTradeFairOpenKey(row) {
|
|
const messe = String(row.messe || '').trim().toLocaleLowerCase('de-DE');
|
|
const city = String(row.stadt || '').trim().toLocaleLowerCase('de-DE');
|
|
return `${messe}|${city}`;
|
|
}
|
|
|
|
function setTradeFairLastOpened(row) {
|
|
const key = getTradeFairOpenKey(row);
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
const nowIso = new Date().toISOString();
|
|
lastOpenedByTradeFair[key] = nowIso;
|
|
persistLastOpenedState();
|
|
return nowIso;
|
|
}
|
|
|
|
function formatDateTime(iso) {
|
|
const parsed = new Date(iso);
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
return 'k.A.';
|
|
}
|
|
return parsed.toLocaleString('de-DE');
|
|
}
|
|
|
|
function formatLastSearched(iso) {
|
|
if (!iso) {
|
|
return 'noch nie';
|
|
}
|
|
return formatDateTime(iso);
|
|
}
|
|
|
|
function getTradeFairHoverTitle(row) {
|
|
const key = getTradeFairOpenKey(row);
|
|
const lastOpened = key ? lastOpenedByTradeFair[key] : '';
|
|
const suffix = lastOpened ? formatDateTime(lastOpened) : 'noch nie';
|
|
return `Als Bookmark-Suche oeffnen (3 Tabs)\\nZuletzt geoeffnet: ${suffix}`;
|
|
}
|
|
|
|
function loadDaysFilterState() {
|
|
try {
|
|
const raw = localStorage.getItem(DAYS_FILTER_STATE_KEY);
|
|
if (typeof raw !== 'string') {
|
|
return '';
|
|
}
|
|
return raw.trim();
|
|
} catch (_error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function persistDaysFilterState() {
|
|
try {
|
|
localStorage.setItem(DAYS_FILTER_STATE_KEY, daysFilterTerm);
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function updateDaysFilterToggleState() {
|
|
if (!daysFilterToggle) {
|
|
return;
|
|
}
|
|
daysFilterToggle.classList.toggle('is-active', Boolean(daysFilterTerm));
|
|
}
|
|
|
|
function setDaysFilterOpen(nextOpen, options = {}) {
|
|
isDaysFilterOpen = Boolean(nextOpen);
|
|
|
|
if (daysFilterContainer) {
|
|
daysFilterContainer.classList.toggle('is-filter-open', isDaysFilterOpen);
|
|
}
|
|
|
|
if (daysFilterToggle) {
|
|
daysFilterToggle.classList.toggle('is-open', isDaysFilterOpen);
|
|
daysFilterToggle.setAttribute('aria-expanded', isDaysFilterOpen ? 'true' : 'false');
|
|
}
|
|
|
|
if (options.focus && isDaysFilterOpen && daysFilterInput) {
|
|
daysFilterInput.focus();
|
|
daysFilterInput.select();
|
|
}
|
|
}
|
|
|
|
function loadSortState() {
|
|
try {
|
|
const raw = localStorage.getItem(SORT_STATE_KEY);
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const normalized = normalizeSortState(parsed);
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
sortKey = normalized.sortKey;
|
|
sortDirection = normalized.sortDirection;
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function persistSortState() {
|
|
try {
|
|
localStorage.setItem(SORT_STATE_KEY, JSON.stringify({
|
|
sortKey,
|
|
sortDirection
|
|
}));
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function updateSortButtonState() {
|
|
sortButtons.forEach((button) => {
|
|
const key = button.dataset.tradeSort;
|
|
const isActive = key === sortKey;
|
|
button.classList.toggle('is-active', isActive);
|
|
const indicator = isActive ? (sortDirection === 'asc' ? '▲' : '▼') : '↕';
|
|
const baseLabel = button.dataset.baseLabel || button.textContent.trim();
|
|
button.textContent = `${baseLabel} ${indicator}`;
|
|
});
|
|
}
|
|
|
|
function clearDropMarkers() {
|
|
Array.from(headerRow.children).forEach((th) => {
|
|
if (th.dataset.columnKey !== draggedColumnKey) {
|
|
th.classList.remove('is-dragging');
|
|
}
|
|
th.classList.remove('is-drop-before', 'is-drop-after');
|
|
});
|
|
}
|
|
|
|
function setupColumnDragAndDrop() {
|
|
Array.from(headerCellsByKey.values()).forEach((th) => {
|
|
th.addEventListener('dragstart', (event) => {
|
|
const key = th.dataset.columnKey;
|
|
if (!key) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
draggedColumnKey = key;
|
|
clearDropMarkers();
|
|
th.classList.add('is-dragging');
|
|
if (event.dataTransfer) {
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
try {
|
|
event.dataTransfer.setData('text/plain', key);
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|
|
});
|
|
|
|
th.addEventListener('dragover', (event) => {
|
|
if (!draggedColumnKey || th.dataset.columnKey === draggedColumnKey) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
clearDropMarkers();
|
|
const rect = th.getBoundingClientRect();
|
|
const insertAfter = event.clientX >= rect.left + (rect.width / 2);
|
|
th.classList.toggle('is-drop-before', !insertAfter);
|
|
th.classList.toggle('is-drop-after', insertAfter);
|
|
if (event.dataTransfer) {
|
|
event.dataTransfer.dropEffect = 'move';
|
|
}
|
|
});
|
|
|
|
th.addEventListener('dragleave', () => {
|
|
th.classList.remove('is-drop-before', 'is-drop-after');
|
|
});
|
|
|
|
th.addEventListener('drop', (event) => {
|
|
event.preventDefault();
|
|
const targetKey = th.dataset.columnKey;
|
|
const sourceKey = draggedColumnKey || (event.dataTransfer ? event.dataTransfer.getData('text/plain') : '');
|
|
|
|
if (!sourceKey || !targetKey || sourceKey === targetKey) {
|
|
draggedColumnKey = null;
|
|
clearDropMarkers();
|
|
return;
|
|
}
|
|
|
|
const sourceTh = headerCellsByKey.get(sourceKey);
|
|
const targetTh = headerCellsByKey.get(targetKey);
|
|
if (!sourceTh || !targetTh) {
|
|
draggedColumnKey = null;
|
|
clearDropMarkers();
|
|
return;
|
|
}
|
|
|
|
const rect = targetTh.getBoundingClientRect();
|
|
const insertAfter = event.clientX >= rect.left + (rect.width / 2);
|
|
if (insertAfter) {
|
|
headerRow.insertBefore(sourceTh, targetTh.nextSibling);
|
|
} else {
|
|
headerRow.insertBefore(sourceTh, targetTh);
|
|
}
|
|
|
|
draggedColumnKey = null;
|
|
clearDropMarkers();
|
|
persistColumnOrder();
|
|
render();
|
|
renderColumnSettingsList();
|
|
});
|
|
|
|
th.addEventListener('dragend', () => {
|
|
draggedColumnKey = null;
|
|
clearDropMarkers();
|
|
});
|
|
});
|
|
}
|
|
|
|
function createCell(content, columnKey) {
|
|
const td = document.createElement('td');
|
|
if (columnKey) {
|
|
td.dataset.column = columnKey;
|
|
}
|
|
td.textContent = content;
|
|
return td;
|
|
}
|
|
|
|
function getTradeFairBookmarkQueries(row) {
|
|
if (Array.isArray(row.bookmark_suchbegriffe) && row.bookmark_suchbegriffe.length) {
|
|
return [...new Set(
|
|
row.bookmark_suchbegriffe
|
|
.map((entry) => String(entry || '').trim())
|
|
.filter(Boolean)
|
|
)];
|
|
}
|
|
|
|
const messe = String(row.messe || '').trim();
|
|
if (!messe) {
|
|
return [];
|
|
}
|
|
|
|
if (messe.includes(' / ')) {
|
|
const split = messe.split(' / ').map((part) => part.trim()).filter(Boolean);
|
|
if (split.length > 1) {
|
|
return [...new Set(split)];
|
|
}
|
|
}
|
|
|
|
return [messe];
|
|
}
|
|
|
|
function openBookmarkLikeFallback(baseQuery) {
|
|
const trimmed = String(baseQuery || '').trim();
|
|
if (!trimmed) {
|
|
return 0;
|
|
}
|
|
|
|
const suffixes = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
|
let opened = 0;
|
|
|
|
suffixes.forEach((suffix) => {
|
|
const query = `${trimmed} ${suffix}`.trim();
|
|
if (typeof window.buildBookmarkSearchUrl === 'function') {
|
|
const built = window.buildBookmarkSearchUrl(query);
|
|
if (built) {
|
|
window.open(built, '_blank', 'noopener');
|
|
opened += 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const url = new URL('https://www.facebook.com/search/posts');
|
|
url.searchParams.set('q', query);
|
|
window.open(url.toString(), '_blank', 'noopener');
|
|
opened += 1;
|
|
});
|
|
|
|
return opened;
|
|
}
|
|
|
|
function openTradeFairSearch(row, explicitBaseQuery) {
|
|
const baseQuery = explicitBaseQuery
|
|
? String(explicitBaseQuery).trim()
|
|
: getTradeFairBookmarkQueries(row)[0];
|
|
if (!baseQuery) {
|
|
return 0;
|
|
}
|
|
|
|
let opened = 0;
|
|
if (typeof window.openBookmarkQueries === 'function') {
|
|
opened = window.openBookmarkQueries(baseQuery);
|
|
}
|
|
|
|
if (opened === 0) {
|
|
opened = openBookmarkLikeFallback(baseQuery);
|
|
}
|
|
|
|
if (opened > 0) {
|
|
setTradeFairLastOpened(row);
|
|
}
|
|
return opened;
|
|
}
|
|
|
|
function createMesseCell(row) {
|
|
const td = document.createElement('td');
|
|
td.dataset.column = 'messe';
|
|
|
|
const bookmarkQueries = getTradeFairBookmarkQueries(row);
|
|
if (!bookmarkQueries.length) {
|
|
td.textContent = row.messe || 'k.A.';
|
|
return td;
|
|
}
|
|
|
|
const linksWrap = document.createElement('div');
|
|
linksWrap.className = 'bookmark-subpage__messe-links';
|
|
|
|
bookmarkQueries.forEach((query, index) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'bookmark-subpage__messe-link';
|
|
button.textContent = query;
|
|
button.title = getTradeFairHoverTitle(row);
|
|
const handleOpenTradeFair = () => {
|
|
const opened = openTradeFairSearch(row, query);
|
|
if (opened > 0) {
|
|
button.title = getTradeFairHoverTitle(row);
|
|
render();
|
|
}
|
|
};
|
|
button.addEventListener('click', handleOpenTradeFair);
|
|
if (typeof window.bindMiddleMouseOpen === 'function') {
|
|
window.bindMiddleMouseOpen(button, handleOpenTradeFair);
|
|
}
|
|
linksWrap.appendChild(button);
|
|
|
|
if (index < bookmarkQueries.length - 1) {
|
|
const separator = document.createElement('span');
|
|
separator.className = 'bookmark-subpage__messe-separator';
|
|
separator.textContent = ' / ';
|
|
linksWrap.appendChild(separator);
|
|
}
|
|
});
|
|
|
|
td.appendChild(linksWrap);
|
|
return td;
|
|
}
|
|
|
|
function createSourceCell(row) {
|
|
const sourceCell = document.createElement('td');
|
|
sourceCell.dataset.column = 'quelle_homepage';
|
|
if (row.quelle_homepage) {
|
|
const link = document.createElement('a');
|
|
link.href = row.quelle_homepage;
|
|
link.target = '_blank';
|
|
link.rel = 'noopener';
|
|
link.textContent = row.quelle_homepage;
|
|
sourceCell.appendChild(link);
|
|
} else {
|
|
sourceCell.textContent = 'k.A.';
|
|
}
|
|
return sourceCell;
|
|
}
|
|
|
|
function createRowCells(row) {
|
|
return {
|
|
tage_bis_start: createCell(Number.isFinite(row.tage_bis_start) ? String(row.tage_bis_start) : 'k.A.', 'tage_bis_start'),
|
|
rang: createCell(String(row.rang), 'rang'),
|
|
messe: createMesseCell(row),
|
|
zuletzt_gesucht_am: createCell(formatLastSearched(row.zuletzt_gesucht_am), 'zuletzt_gesucht_am'),
|
|
thema: createCell(row.thema, 'thema'),
|
|
stadt: createCell(row.stadt, 'stadt'),
|
|
bundesland: createCell(row.bundesland, 'bundesland'),
|
|
termin: createCell(formatTermRange(row.termin_start, row.termin_ende), 'termin'),
|
|
besucher: createCell(formatNumber(row.besucher), 'besucher'),
|
|
besucher_jahr: createCell(formatNumber(row.besucher_jahr), 'besucher_jahr'),
|
|
besucher_status: createCell(row.besucher_status, 'besucher_status'),
|
|
ausstellungsflaeche_m2: createCell(formatNumber(row.ausstellungsflaeche_m2), 'ausstellungsflaeche_m2'),
|
|
ticketpreis_we_eur: createCell(formatPrice(row.ticketpreis_we_eur), 'ticketpreis_we_eur'),
|
|
ticketpreis_unterderwoche_eur: createCell(formatPrice(row.ticketpreis_unterderwoche_eur), 'ticketpreis_unterderwoche_eur'),
|
|
notiz: createCell(row.notiz, 'notiz'),
|
|
quelle_homepage: createSourceCell(row)
|
|
};
|
|
}
|
|
|
|
function render() {
|
|
const normalizedRows = EFFECTIVE_TRADE_FAIRS.map(normalizeRow);
|
|
const parsedDaysFilter = parseDaysFilter(daysFilterTerm);
|
|
const filtered = normalizedRows.filter(
|
|
(row) => matchesSearch(row, searchTerm) && matchesDaysFilter(row.tage_bis_start, parsedDaysFilter)
|
|
);
|
|
const sorted = sortRows(filtered);
|
|
const visibleColumnOrder = getVisibleColumnOrder();
|
|
|
|
tableBody.innerHTML = '';
|
|
|
|
if (!sorted.length) {
|
|
const emptyRow = document.createElement('tr');
|
|
const emptyCell = document.createElement('td');
|
|
emptyCell.colSpan = visibleColumnOrder.length;
|
|
emptyCell.textContent = 'Keine Messe zum Suchbegriff gefunden.';
|
|
emptyRow.appendChild(emptyCell);
|
|
tableBody.appendChild(emptyRow);
|
|
} else {
|
|
sorted.forEach((row) => {
|
|
const tr = document.createElement('tr');
|
|
const rowCells = createRowCells(row);
|
|
visibleColumnOrder.forEach((columnKey) => {
|
|
const cell = rowCells[columnKey] || createCell('k.A.', columnKey);
|
|
tr.appendChild(cell);
|
|
});
|
|
|
|
tableBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
const activeSortButton = sortButtons.find((button) => button.dataset.tradeSort === sortKey);
|
|
const sortLabel = activeSortButton?.dataset.baseLabel || sortKey;
|
|
const daysFilterMeta = getDaysFilterMetaLabel(parsedDaysFilter);
|
|
meta.textContent = `${sorted.length} von ${EFFECTIVE_TRADE_FAIRS.length} Messen | Sortierung: ${sortLabel} (${sortDirection === 'asc' ? 'aufsteigend' : 'absteigend'})${daysFilterMeta ? ` | ${daysFilterMeta}` : ''}`;
|
|
}
|
|
|
|
sortButtons.forEach((button) => {
|
|
const label = button.textContent.trim();
|
|
button.dataset.baseLabel = label;
|
|
button.addEventListener('click', () => {
|
|
const key = button.dataset.tradeSort;
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
if (sortKey === key) {
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortKey = key;
|
|
sortDirection = key === 'rang' ? 'asc' : 'desc';
|
|
}
|
|
|
|
persistSortState();
|
|
sortButtons.forEach((btn) => {
|
|
const baseLabel = btn.dataset.baseLabel || btn.textContent;
|
|
btn.textContent = baseLabel;
|
|
});
|
|
updateSortButtonState();
|
|
render();
|
|
});
|
|
});
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
searchTerm = searchInput.value.trim().toLocaleLowerCase('de-DE');
|
|
render();
|
|
});
|
|
|
|
if (daysFilterToggle) {
|
|
daysFilterToggle.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setDaysFilterOpen(!isDaysFilterOpen, { focus: true });
|
|
});
|
|
}
|
|
|
|
if (daysFilterInput) {
|
|
daysFilterInput.addEventListener('input', () => {
|
|
daysFilterTerm = daysFilterInput.value.trim();
|
|
persistDaysFilterState();
|
|
updateDaysFilterToggleState();
|
|
render();
|
|
});
|
|
|
|
daysFilterInput.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
setDaysFilterOpen(false);
|
|
if (daysFilterToggle) {
|
|
daysFilterToggle.focus();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (daysFilterContainer) {
|
|
document.addEventListener('click', (event) => {
|
|
if (!isDaysFilterOpen) {
|
|
return;
|
|
}
|
|
const target = event.target;
|
|
if (!(target instanceof Node)) {
|
|
return;
|
|
}
|
|
if (!daysFilterContainer.contains(target)) {
|
|
setDaysFilterOpen(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
daysFilterTerm = loadDaysFilterState();
|
|
if (daysFilterInput) {
|
|
daysFilterInput.value = daysFilterTerm;
|
|
}
|
|
updateDaysFilterToggleState();
|
|
setDaysFilterOpen(false);
|
|
|
|
lastOpenedByTradeFair = loadLastOpenedState();
|
|
loadColumnOrder();
|
|
loadColumnVisibility();
|
|
applyColumnVisibilityToHeader();
|
|
setupColumnDragAndDrop();
|
|
setupColumnSettingsModal();
|
|
loadSortState();
|
|
ensureValidSortColumn();
|
|
updateSortButtonState();
|
|
render();
|
|
})();
|