Files
PostTracker/web/trade-fairs.js

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();
})();