DanceMap AI DanceMap AI
Bot
Velkommen til DanceMap AI! 💃 Find salsa, bachata, kizomba og andre dance events i Danmark! 🔍 Søg: "salsa i københavn" eller "events i weekenden" 📝 Tilføj manglende event: Skriv "tilføj event" Hvad leder du efter? 😊
Bot
- // Google Apps Script - DanceMap AI v2.1 - BULLETPROOF! 🛡️ // Features: Perfekt context memory, "flere" command, smart suggestions const SHEET_ID = "1nECcg7qrlfvQabDaIhPheXeQ0gu4_fdaQ4FhIOjLT_k"; const SALSA_CALENDAR_ID = "salsadenmark@gmail.com"; const CACHE_HOURS = 24; const PENDING_SHEET_NAME = "Pending"; // New sheet for user submissions // Session memory - gemmer brugerens sidste søgning OG results // Bruger PropertiesService for at gemme mellem requests function getSessionContext(sessionId) { const props = PropertiesService.getScriptProperties(); const key = "session_" + sessionId; const data = props.getProperty(key); if (data) { try { return JSON.parse(data); } catch (e) { return { by: null, dansetype: null, lastResults: [], offset: 0 }; } } return { by: null, dansetype: null, lastResults: [], offset: 0 }; } function setSessionContext(sessionId, context) { const props = PropertiesService.getScriptProperties(); const key = "session_" + sessionId; props.setProperty(key, JSON.stringify(context)); } function doGet(e) { const message = e.parameter.message || ""; const sessionId = e.parameter.sessionId || "default"; try { const sheetEvents = getSheetEvents(); const calendarEvents = getCachedCalendarEvents(); let allEvents = [...sheetEvents, ...calendarEvents]; // SMART dubletter fjernelse const uniqueEvents = []; const seen = new Set(); allEvents.forEach(event => { const dato = String(event.Dato || event.date || ''); const tid = String(event.Starttid || event.time || '').substring(0, 5); const sted = String(event.Sted || event.location || ''); // Normalize venue for better duplicate detection const normalizedSted = sted.toLowerCase() .replace(/\s+/g, '') // Remove all spaces .replace(/[,.-]/g, ''); // Remove punctuation const dateOnly = dato.split(' ')[0].split('T')[0]; const key = `${dateOnly}-${tid}-${normalizedSted}`; if (!seen.has(key)) { seen.add(key); uniqueEvents.push(event); } else { Logger.log(`🔄 Duplikat fjernet: ${event.Dansetype || event.title} kl. ${tid}`); } }); Logger.log(`📊 Total events: ${uniqueEvents.length}`); // Generer smart response med context const reply = generateSmartResponse(message, uniqueEvents, sessionId); return ContentService .createTextOutput(JSON.stringify({ reply: reply })) .setMimeType(ContentService.MimeType.JSON); } catch (error) { Logger.log("Error: " + error); return ContentService .createTextOutput(JSON.stringify({ reply: "Beklager, der skete en fejl. Prøv igen senere." })) .setMimeType(ContentService.MimeType.JSON); } } // Cached calendar events function getCachedCalendarEvents() { const cache = CacheService.getScriptCache(); const cached = cache.get("salsa_calendar"); if (cached) { Logger.log("✅ Bruger cached calendar data"); return JSON.parse(cached); } Logger.log("⚠️ Cache udløbet - henter ny data"); const events = getSalsaCalendarEvents(); cache.put("salsa_calendar", JSON.stringify(events), 86400); return events; } function getSalsaCalendarEvents() { try { const delay = Math.floor(Math.random() * 5000) + 3000; Utilities.sleep(delay); const calendar = CalendarApp.getCalendarById(SALSA_CALENDAR_ID); if (!calendar) return []; const now = new Date(); const fourMonthsLater = new Date(now.getTime() + 120 * 24 * 60 * 60 * 1000); const calendarEvents = calendar.getEvents(now, fourMonthsLater); Logger.log("✅ Fandt " + calendarEvents.length + " calendar events"); const expandedEvents = []; calendarEvents.forEach(event => { const startTime = event.getStartTime(); const location = event.getLocation() || ""; const title = event.getTitle(); const description = event.getDescription() || ""; let by = extractCity(location + " " + description); let dansetype = title; // Keep full title by default // Only simplify if it's a generic title const lowerTitle = title.toLowerCase(); if (lowerTitle === "salsa" || lowerTitle === "bachata" || lowerTitle === "kizomba") { // Generic title - keep as is dansetype = title; } else if (lowerTitle.includes("salsa") && lowerTitle.includes("bachata")) { // Has both - keep full title dansetype = title; } else { // Keep full title for specific events like "CHRISTMAS CSA Latin Social" dansetype = title; } // CLEAN location data - AGGRESSIVE cleaning let cleanedLocation = location; if (cleanedLocation) { // First: Remove everything after and including postal code cleanedLocation = cleanedLocation.replace(/\d{4}.*$/g, ''); // Remove "SE-" prefix (Swedish postal codes) cleanedLocation = cleanedLocation.replace(/SE-\d+.*$/g, ''); // Clean up trailing commas and spaces cleanedLocation = cleanedLocation.replace(/,\s*$/g, ''); cleanedLocation = cleanedLocation.replace(/\s+/g, ' '); cleanedLocation = cleanedLocation.trim(); } expandedEvents.push({ Dansetype: dansetype, Dato: Utilities.formatDate(startTime, "Europe/Copenhagen", "yyyy-MM-dd HH:mm"), Starttid: Utilities.formatDate(startTime, "Europe/Copenhagen", "HH:mm"), Sted: cleanedLocation || location, By: by, source: "salsa.dk" }); }); return expandedEvents; } catch (error) { Logger.log("❌ Calendar fejl: " + error); return []; } } function extractCity(text) { const cities = [ "københavn", "kbh", "frederiksberg", "nørrebro", "østerbro", "vesterbro", "amager", "aarhus", "århus", "aalborg", "ålborg", "odense", "esbjerg", "randers", "kolding", "vejle", "horsens", "roskilde", "helsingør", "næstved", "viborg", "silkeborg", "herning", "fredericia", "køge" ]; const lowerText = text.toLowerCase(); for (let city of cities) { if (lowerText.includes(city)) { return city.charAt(0).toUpperCase() + city.slice(1); } } return "Danmark"; } function getSheetEvents() { try { const ss = SpreadsheetApp.openById(SHEET_ID); const sheet = ss.getSheetByName("Events"); if (!sheet) return []; const data = sheet.getDataRange().getValues(); if (data.length === 0) return []; const headers = data.shift(); return data.map(row => { let obj = { source: 'sheet' }; headers.forEach((header, index) => { obj[header] = row[index]; }); return obj; }).filter(event => { return event.Dato || event.date || event.Dansetype || event.title; }); } catch (err) { Logger.log("❌ Sheet error: " + err); return []; } } // ============================================ // 🧠 BULLETPROOF SMART NLP & RESPONSE GENERATOR // ============================================ function generateSmartResponse(message, events, sessionId) { const msg = message.toLowerCase().trim(); if (events.length === 0) { return "Jeg kunne desværre ikke finde nogle events lige nu. Prøv igen senere!"; } // Hent session context let context = getSessionContext(sessionId); // Check for "flere" command if (msg === "flere" || msg === "more" || msg === "vis flere") { if (context.lastResults && context.lastResults.length > 0) { context.offset += 10; const moreEvents = context.lastResults.slice(context.offset, context.offset + 10); if (moreEvents.length === 0) { return "Det var alle events! 🎉 Prøv at søge efter noget andet."; } setSessionContext(sessionId, context); return formatEventResponse(moreEvents, context.lastIntent, context.lastResults.length, context.offset); } else { return "Søg efter events først! Prøv fx 'salsa i københavn' 😊"; } } // Parse user intent const intent = parseIntent(msg); Logger.log("🧠 Intent: " + JSON.stringify(intent)); // SMART CONTEXT: Hvis brugeren IKKE specificerer by/dansetype, brug context if (context.by && !intent.by && intent.type === "search") { intent.by = context.by; Logger.log("🧠 Bruger context by: " + intent.by); } if (context.dansetype && !intent.dansetype && intent.type === "search") { // Kun hvis de ikke specifikt spørger om en ANDEN dansetype if (!msg.includes("salsa") && !msg.includes("bachata") && !msg.includes("kizomba")) { intent.dansetype = context.dansetype; Logger.log("🧠 Bruger context dansetype: " + intent.dansetype); } } // Hilsner if (intent.type === "greeting") { return "Hej! 👋 Jeg kan hjælpe dig finde dance events i Danmark.\n\nSpørg mig fx:\n• 'Salsa i København i aften'\n• 'Bachata denne weekend'\n• 'Hvad sker der?'\n\nHvad vil du gerne danse? 💃"; } // Help if (intent.type === "help") { return "Jeg finder salsa, bachata og kizomba events for dig! 🎉\n\nJeg forstår spørgsmål som:\n• 'Salsa i København i dag'\n• 'Bachata events denne uge'\n• 'Hvad kan jeg lave i weekenden?'\n• 'Gratis events'\n\nBare spørg naturligt! 😊"; } // Submit event prompt if (intent.type === "submit_prompt") { return "📝 **Indsend event!**\n\n" + "Send mig disse oplysninger:\n\n" + "📅 Dato: (fx '6. december' eller '2024-12-06')\n" + "🕐 Tidspunkt: (fx '20:00')\n" + "💃 Type: (Salsa/Bachata/Kizomba)\n" + "📍 Sted: (fx 'Nordhus')\n" + "📍 Adresse: (fx 'Århusgade 124A')\n" + "🏙️ By: (fx 'København')\n" + "🎫 Link: (valgfrit - Facebook/Billetto link)\n\n" + "Skriv det bare i én besked, jeg finder ud af resten! 😊"; } // Generel oversigt if (intent.type === "overview") { const result = showUpcomingEvents(events, 10); return result; } // Hvis query er uforståelig eller for kort - giv hjælp! if (msg.length < 3 || (!intent.dansetype && !intent.by && !intent.timeframe && intent.type === "search")) { return "🤔 Jeg forstod ikke helt. Prøv fx:\n\n" + "• 'Salsa i København'\n" + "• 'Bachata i weekenden'\n" + "• 'Hvad sker der i dag?'\n" + "• 'Vis alle events'\n\n" + "Eller skriv 'hjælp' for flere eksempler! 😊"; } // Check if message looks like event submission const submissionKeywords = ['dato:', 'tidspunkt:', 'kl.', 'sted:', 'adresse:', 'type:', 'link:', 'facebook.com', 'billetto']; const hasSubmissionKeywords = submissionKeywords.some(kw => msg.includes(kw)); // Or if it has a date pattern and location info const hasDatePattern = msg.match(/\d{1,2}\.?\s*(jan|feb|mar|apr|maj|jun|jul|aug|sep|okt|nov|dec|januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december)/i); const hasTimePattern = msg.match(/\d{1,2}[:\.]\d{2}/); if (hasSubmissionKeywords || (hasDatePattern && hasTimePattern)) { const submissionResult = tryParseAndSubmitEvent(message); return submissionResult; } // Filter events baseret på intent let filteredEvents = filterEventsByIntent(events, intent); // Hvis ingen results - giv intelligente forslag if (filteredEvents.length === 0) { return handleNoResults(events, intent, context); } // Sort by date filteredEvents.sort((a, b) => { const dateA = parseEventDate(a.Dato || a.date) || new Date(0); const dateB = parseEventDate(b.Dato || b.date) || new Date(0); return dateA - dateB; }); // GEM CONTEXT for næste gang (VIGTIGT!) if (intent.by) context.by = intent.by; if (intent.dansetype) context.dansetype = intent.dansetype; context.lastResults = filteredEvents; context.lastIntent = intent; context.offset = 0; // Reset offset setSessionContext(sessionId, context); Logger.log("💾 Gemt context: by=" + context.by + ", dansetype=" + context.dansetype); // Format response (vis første 10) let response = formatEventResponse(filteredEvents.slice(0, 10), intent, filteredEvents.length, 0); // Tilføj smart follow-up forslag (MED CONTEXT!) response += generateFollowUpSuggestions(filteredEvents, intent, context); return response; } // Parse user intent fra natural language function parseIntent(msg) { const intent = { type: "search", by: null, dansetype: null, timeframe: null, pris: null }; // Greeting detection if (msg.match(/^(hej|hey|hi|hallo|goddag|yo)$/)) { intent.type = "greeting"; return intent; } // Help detection if (msg.includes("hvad kan du") || msg.includes("help") || msg.includes("hjælp")) { intent.type = "help"; return intent; } // Submit event detection if (msg.includes("tilføj event") || msg.includes("submit event") || msg.includes("mangler event") || msg.includes("indsend event") || msg.includes("jeg kender et event") || msg.includes("nyt event")) { intent.type = "submit_prompt"; return intent; } // Overview detection if (msg.includes("hvad sker") || msg.includes("vis events") || msg === "events") { intent.type = "overview"; return intent; } // Natural language patterns for time if (msg.match(/(i aften|tonight|i dag|idag)/)) { intent.timeframe = "today"; } else if (msg.match(/(i morgen|imorgen|tomorrow)/)) { intent.timeframe = "tomorrow"; } else if (msg.match(/(weekend|weekenden|lørdag|søndag)/)) { intent.timeframe = "weekend"; } else if (msg.match(/(denne uge|ugen|this week)/)) { intent.timeframe = "week"; } else if (msg.match(/(denne måned|måneden|this month)/)) { intent.timeframe = "month"; } // Month detection const monthMap = { "januar": 0, "februar": 1, "marts": 2, "april": 3, "maj": 4, "juni": 5, "juli": 6, "august": 7, "september": 8, "oktober": 9, "november": 10, "december": 11 }; for (const [monthName, monthIndex] of Object.entries(monthMap)) { if (msg.includes(monthName)) { intent.timeframe = "month:" + monthIndex; break; } } // Dansetype detection if (msg.match(/salsa/) && !msg.match(/bachata/)) { intent.dansetype = "salsa"; } else if (msg.match(/bachata/) && !msg.match(/salsa/)) { intent.dansetype = "bachata"; } else if (msg.match(/kizomba/)) { intent.dansetype = "kizomba"; } else if (msg.match(/salsa.*bachata|bachata.*salsa/)) { intent.dansetype = "both"; } // By detection const cityMap = { københavn: ["københavn", "kbh", "cph", "køben", "frederiks", "nørrebro", "østerbro", "vesterbro", "amager"], aarhus: ["aarhus", "århus"], aalborg: ["aalborg", "ålborg"], odense: ["odense"] }; for (const [city, keywords] of Object.entries(cityMap)) { if (keywords.some(keyword => msg.includes(keyword))) { intent.by = city; break; } } // Pris detection if (msg.match(/gratis|free|ingen pris/)) { intent.pris = "gratis"; } return intent; } function filterEventsByIntent(events, intent) { let filtered = events; const today = new Date(); today.setHours(0, 0, 0, 0); // Fjern gamle events filtered = filtered.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); return eventDate && eventDate >= today; }); // Filter på tid if (intent.timeframe) { if (intent.timeframe === "today") { const todayStr = Utilities.formatDate(today, "Europe/Copenhagen", "yyyy-MM-dd"); filtered = filtered.filter(e => { const eventDate = formatEventDate(e.Dato || e.date); return eventDate === todayStr; }); } else if (intent.timeframe === "tomorrow") { const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000); const tomorrowStr = Utilities.formatDate(tomorrow, "Europe/Copenhagen", "yyyy-MM-dd"); filtered = filtered.filter(e => { const eventDate = formatEventDate(e.Dato || e.date); return eventDate === tomorrowStr; }); } else if (intent.timeframe === "weekend") { filtered = filtered.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); if (!eventDate) return false; const dayOfWeek = eventDate.getDay(); return dayOfWeek === 5 || dayOfWeek === 6 || dayOfWeek === 0; }); } else if (intent.timeframe === "week") { const weekLater = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000); filtered = filtered.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); if (!eventDate) return false; return eventDate >= today && eventDate <= weekLater; }); } else if (intent.timeframe === "month") { const currentMonth = today.getMonth(); const currentYear = today.getFullYear(); filtered = filtered.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); if (!eventDate) return false; return eventDate.getMonth() === currentMonth && eventDate.getFullYear() === currentYear; }); } else if (intent.timeframe.startsWith("month:")) { const monthIndex = parseInt(intent.timeframe.split(":")[1]); filtered = filtered.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); if (!eventDate) return false; return eventDate.getMonth() === monthIndex; }); } } // Filter på dansetype if (intent.dansetype) { if (intent.dansetype === "both") { filtered = filtered.filter(e => { const dansetype = (e.Dansetype || e.title || "").toLowerCase(); return dansetype.includes("salsa") || dansetype.includes("bachata"); }); } else { filtered = filtered.filter(e => { const dansetype = (e.Dansetype || e.title || "").toLowerCase(); return dansetype.includes(intent.dansetype); }); } } // Filter på by if (intent.by) { const cityKeywords = { københavn: ["københavn", "kbh", "frederiksberg", "nørrebro", "østerbro", "vesterbro", "amager"], aarhus: ["aarhus", "århus"], aalborg: ["aalborg", "ålborg"], odense: ["odense"] }; const keywords = cityKeywords[intent.by] || [intent.by]; filtered = filtered.filter(e => { const by = (e.By || e.by || e.location || "").toLowerCase(); return keywords.some(keyword => by.includes(keyword)); }); } // Filter på pris if (intent.pris === "gratis") { filtered = filtered.filter(e => { const pris = (e.Pris || "").toLowerCase(); return pris.includes("gratis") || pris.includes("free") || pris === ""; }); } return filtered; } function handleNoResults(allEvents, intent, context) { const today = new Date(); const upcoming = allEvents.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); return eventDate && eventDate >= today; }).sort((a, b) => { const dateA = parseEventDate(a.Dato || a.date) || new Date(0); const dateB = parseEventDate(b.Dato || b.date) || new Date(0); return dateA - dateB; }); if (upcoming.length === 0) { return "Jeg har desværre ingen kommende events. Prøv igen senere! 📅"; } const nextEvent = upcoming[0]; const nextDate = parseEventDate(nextEvent.Dato || nextEvent.date); const whenText = formatWhenText(nextDate); let suggestion = `Jeg fandt desværre ingen events`; if (intent.dansetype) suggestion += ` med ${intent.dansetype}`; if (intent.by) suggestion += ` i ${intent.by}`; if (intent.timeframe) { // Convert timeframe to Danish const timeframeDa = { 'today': 'i dag', 'tomorrow': 'i morgen', 'weekend': 'i weekenden', 'week': 'denne uge', 'month': 'denne måned' }; suggestion += ` ${timeframeDa[intent.timeframe] || intent.timeframe}`; } suggestion += `.\n\nMen ${whenText} er der ${nextEvent.Dansetype} på ${nextEvent.Sted}`; if (nextEvent.By) suggestion += ` (${nextEvent.By})`; suggestion += `! 💃\n\n`; // Add helpful suggestions suggestion += `💡 Prøv fx:\n`; suggestion += `• 'Vis alle events'\n`; if (intent.by) suggestion += `• 'Salsa i ${intent.by}' (uden tidsbegrænsning)\n`; if (intent.dansetype) suggestion += `• '${intent.dansetype} i weekenden'\n`; if (!intent.by) suggestion += `• 'Events i København'\n`; return suggestion; } function formatEventResponse(events, intent, totalCount, offset) { let response = "🎉 "; if (intent.dansetype) response += `${intent.dansetype.charAt(0).toUpperCase() + intent.dansetype.slice(1)} events`; else response += "Events"; if (intent.by) response += ` i ${intent.by.charAt(0).toUpperCase() + intent.by.slice(1)}`; if (intent.timeframe && !intent.timeframe.startsWith("month:")) { response += ` ${intent.timeframe}`; } response += ":\n\n"; events.forEach((event, i) => { const dansetype = event.Dansetype || event.title || "Event"; const dato = event.Dato || event.date || "TBA"; const starttid = event.Starttid || event.time || ""; const sluttid = event.Sluttid || ""; const sted = event.Sted || event.location || ""; const adresse = event.Adresse || ""; const by = event.By || event.by || ""; const pris = event.Pris || ""; const eventDate = parseEventDate(dato); let datoFormatted = dato; if (eventDate) { const weekdayDa = ["Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag"]; const monthsDa = ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"]; const dayName = weekdayDa[eventDate.getDay()]; const day = eventDate.getDate(); const month = monthsDa[eventDate.getMonth()]; datoFormatted = `${dayName} ${day}. ${month}`; } // Capitalize dansetype properly const capitalizedDansetype = dansetype.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() ).join(' '); // Build time string let timeStr = ''; if (starttid) { timeStr = ` kl. ${starttid}`; if (sluttid && sluttid !== "00:00" && sluttid !== "00.00" && sluttid !== "") { timeStr += `-${sluttid}`; } } // First line: Date + Time + Dance Type response += `${datoFormatted}${timeStr} - ${capitalizedDansetype}\n`; // Second line: Location (City first if exists, then venue/address) let locationLine = '📍 '; // Capitalize city properly const capitalizedCity = by ? by.charAt(0).toUpperCase() + by.slice(1).toLowerCase() : ''; if (capitalizedCity && capitalizedCity !== 'Danmark') { locationLine += `${capitalizedCity} - `; } // Add venue and address (COMPLETELY cleaned) let locationParts = []; if (sted && sted.trim()) { locationParts.push(sted.trim()); } if (adresse && adresse.trim() && adresse.trim() !== sted.trim()) { let cleanAddress = adresse.trim(); // Remove postal codes (4 digits) cleanAddress = cleanAddress.replace(/\d{4}/g, ''); // Remove "København N/S/Ø/V" etc. cleanAddress = cleanAddress.replace(/København\s*[NSØV]?/gi, ''); cleanAddress = cleanAddress.replace(/Aarhus\s*[NSØV]?/gi, ''); cleanAddress = cleanAddress.replace(/Aalborg\s*[NSØV]?/gi, ''); cleanAddress = cleanAddress.replace(/Odense\s*[NSØV]?/gi, ''); // Remove "Denmark" or "Danmark" cleanAddress = cleanAddress.replace(/,?\s*Denmark$/gi, ''); cleanAddress = cleanAddress.replace(/,?\s*Danmark$/gi, ''); // Remove multiple commas and spaces cleanAddress = cleanAddress.replace(/,\s*,/g, ','); cleanAddress = cleanAddress.replace(/\s+,/g, ','); cleanAddress = cleanAddress.replace(/,\s+$/g, ''); cleanAddress = cleanAddress.replace(/^\s*,/g, ''); cleanAddress = cleanAddress.trim(); // Only add if there's something left and it's different from venue if (cleanAddress && cleanAddress !== sted.trim() && cleanAddress.length > 0) { locationParts.push(cleanAddress); } } if (locationParts.length > 0) { locationLine += locationParts.join(', '); } response += `${locationLine}\n`; // Optional: Price line (if exists) if (pris && pris.trim()) { response += `💰 ${pris}\n`; } response += `\n`; }); // BULLETPROOF "flere" besked const remaining = totalCount - offset - events.length; if (remaining > 0) { response += `📋 Viser ${offset + 1}-${offset + events.length} af ${totalCount} events\n`; response += `💬 Skriv 'flere' for at se de næste ${Math.min(remaining, 10)} events\n\n`; } else if (totalCount > 10) { response += `📋 Det var alle ${totalCount} events! 🎉\n\n`; } return response; } function generateFollowUpSuggestions(events, intent, context) { let suggestions = "💡 "; const addedSuggestions = []; // Hvis de søgte salsa, foreslå bachata (MED SAMME BY!) if (intent.dansetype === "salsa" && context.by) { addedSuggestions.push(`Vil du se bachata i ${context.by.charAt(0).toUpperCase() + context.by.slice(1)}? Skriv 'bachata'`); } else if (intent.dansetype === "bachata" && context.by) { addedSuggestions.push(`Vil du se salsa i ${context.by.charAt(0).toUpperCase() + context.by.slice(1)}? Skriv 'salsa'`); } // Hvis de søgte i specifik by, foreslå anden by if (intent.by === "københavn") { const aarhusCount = events.filter(e => (e.By || "").toLowerCase().includes("aarhus")).length; if (aarhusCount > 0) { addedSuggestions.push(`${aarhusCount} events i Aarhus`); } } // Hvis de søgte i dag, foreslå i morgen if (intent.timeframe === "today") { addedSuggestions.push(`Prøv 'i morgen' for flere events`); } if (addedSuggestions.length > 0) { suggestions += addedSuggestions.join(" • "); return "\n" + suggestions; } return ""; } // Helper functions function parseEventDate(dateValue) { if (!dateValue) return null; try { if (dateValue instanceof Date) return dateValue; if (typeof dateValue === 'string') { const dateStr = dateValue.split('T')[0].split(' ')[0]; return new Date(dateStr); } return new Date(dateValue); } catch (e) { return null; } } function formatEventDate(dateValue) { const date = parseEventDate(dateValue); if (!date) return ""; return Utilities.formatDate(date, "Europe/Copenhagen", "yyyy-MM-dd"); } function formatWhenText(date) { if (!date) return "snart"; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const eventDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const diffDays = Math.floor((eventDay - today) / (1000 * 60 * 60 * 24)); const weekdayDa = ["søndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "lørdag"]; const monthsDa = ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"]; if (diffDays === 0) return "i dag"; if (diffDays === 1) return "i morgen"; if (diffDays === 2) return "i overmorgen"; if (diffDays <= 6) return weekdayDa[date.getDay()]; return `${weekdayDa[date.getDay()]} ${date.getDate()}. ${monthsDa[date.getMonth()]}`; } function showUpcomingEvents(events, limit) { const today = new Date(); const upcoming = events.filter(e => { const eventDate = parseEventDate(e.Dato || e.date); return eventDate && eventDate >= today; }).sort((a, b) => { const dateA = parseEventDate(a.Dato || a.date) || new Date(0); const dateB = parseEventDate(b.Dato || b.date) || new Date(0); return dateA - dateB; }); if (upcoming.length === 0) { return "Der er desværre ingen kommende events lige nu."; } return formatEventResponse(upcoming.slice(0, limit), { type: "overview" }, upcoming.length, 0) + "\n💃 Spørg efter specifikke events! 🕺"; } // ============================================ // 🎫 AUTOMATIC TICKET LINK SCRAPER // Tilføj denne kode til BUNDEN af din dancemap-bulletproof.js // ============================================ // Kendte billetplatforme const TICKET_PLATFORMS = { 'billetto.dk': 'Billetto', 'billetto.com': 'Billetto', 'facebook.com/events': 'Facebook', 'fb.me': 'Facebook', 'ticketmaster.dk': 'Ticketmaster', 'eventbrite.dk': 'Eventbrite', 'eventbrite.com': 'Eventbrite' }; // Kendte arrangør websites const KNOWN_ORGANIZERS = { 'Copenhagen Salsa Academy': 'https://copenhagensalsaacademy.dk', 'CSA': 'https://copenhagensalsaacademy.dk', 'Salsa Libre Copenhagen': 'https://salsalibre.dk', 'Salsa Libre': 'https://salsalibre.dk', 'Kizomba Copenhagen': 'https://www.kizombacopenhagen.com', 'Next House Copenhagen': 'https://nexthousecopenhagen.com', 'Dogville Salsa': 'https://dogvillesalsa.dk', 'AK Dance': 'https://akdance.dk' }; /** * HOVEDFUNKTION - Kør denne manuelt eller via trigger * Finder billetlinks for alle events uden links */ function findAllTicketLinks() { Logger.log("🎫 ===== STARTER BILLETLINK SCRAPING ====="); try { const ss = SpreadsheetApp.openById(SHEET_ID); const sheet = ss.getSheetByName("Events"); if (!sheet) { Logger.log("❌ Sheet 'Events' ikke fundet!"); return; } const data = sheet.getDataRange().getValues(); const headers = data[0]; // Find kolonne indices const linkColIndex = headers.indexOf('Link'); const arrangørColIndex = headers.indexOf('Arrangør'); const dansetypeColIndex = headers.indexOf('Dansetype'); const stedColIndex = headers.indexOf('Sted'); if (linkColIndex === -1) { Logger.log("❌ Kolonne 'Link' ikke fundet!"); return; } let foundCount = 0; let checkedCount = 0; let skippedCount = 0; // Gå igennem alle events (start fra række 2 for at skippe header) for (let i = 1; i < data.length && i < 50; i++) { // Max 50 events per kørsel const row = data[i]; const currentLink = row[linkColIndex]; // Skip hvis der allerede er et link if (currentLink && currentLink.toString().trim() !== '') { skippedCount++; continue; } const arrangør = row[arrangørColIndex] || ''; const dansetype = row[dansetypeColIndex] || ''; const sted = row[stedColIndex] || ''; if (!arrangør && !sted) { continue; // Ikke nok info til at søge } checkedCount++; Logger.log(`\n🔍 Søger link for event #${i + 1}: ${dansetype} (${arrangør})`); // Prøv at finde billetlink const ticketLink = findTicketLink(dansetype, arrangør, sted); if (ticketLink) { // Opdater sheet sheet.getRange(i + 1, linkColIndex + 1).setValue(ticketLink); foundCount++; Logger.log(`✅ Fandt og gemte link: ${ticketLink}`); // Vent lidt mellem requests for at være høflig Utilities.sleep(2000); } else { Logger.log(`⚠️ Ingen link fundet`); } } Logger.log("\n📊 ===== RESULTATER ====="); Logger.log(`✅ Fandt nye links: ${foundCount}`); Logger.log(`🔍 Tjekkede events: ${checkedCount}`); Logger.log(`⏭️ Havde allerede link: ${skippedCount}`); Logger.log("🎉 ===== FÆRDIG ====="); } catch (error) { Logger.log("❌ FEJL: " + error); } } /** * Find billetlink for et specifikt event */ function findTicketLink(eventName, organizer, venue) { // Strategi 1: Check om arrangør har kendt website if (organizer && KNOWN_ORGANIZERS[organizer]) { const link = scrapeOrganizerWebsite(KNOWN_ORGANIZERS[organizer], eventName); if (link) return link; } // Strategi 2: Søg på Google efter event + billetter const searchLink = searchForTicketLink(eventName, organizer, venue); if (searchLink) return searchLink; return null; } /** * Scrape arrangørens website for billetlink */ function scrapeOrganizerWebsite(url, eventName) { try { const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true }); if (response.getResponseCode() !== 200) { return null; } const html = response.getContentText(); // Søg efter billetplatforme i HTML for (const [platform, name] of Object.entries(TICKET_PLATFORMS)) { if (html.toLowerCase().includes(platform)) { // Prøv at ekstraktere specifikt link const linkMatch = extractLinkFromHtml(html, platform); if (linkMatch) { Logger.log(` 📌 Fandt ${name} link på ${url}`); return linkMatch; } } } return null; } catch (error) { Logger.log(` ⚠️ Kunne ikke scrape ${url}: ${error}`); return null; } } /** * Ekstrahér specifikt link fra HTML */ function extractLinkFromHtml(html, platform) { // Simple regex til at finde URLs const urlRegex = new RegExp(`https?://[^\\s"'<>]*${platform.replace('.', '\\.')}[^\\s"'<>]*`, 'i'); const match = html.match(urlRegex); if (match) { let url = match[0]; // Rens URL url = url.replace(/[)}>"\]]+$/, ''); // Fjern trailing special chars return url; } return null; } /** * Søg efter billetlink via Google Custom Search * (Simpel version - kan forbedres med Custom Search API) */ function searchForTicketLink(eventName, organizer, venue) { try { // Byg søgequery let query = ''; if (eventName) query += eventName + ' '; if (organizer) query += organizer + ' '; query += 'billetter'; // Søg efter kendte platforme const platforms = ['billetto.dk', 'facebook.com/events']; for (const platform of platforms) { const searchQuery = `${query} site:${platform}`; // Note: Dette er en simpel version // For bedre resultater, brug Google Custom Search API Logger.log(` 🔍 Søger: ${searchQuery}`); // Her kunne vi bruge Google Custom Search API // For nu returnerer vi null og logger søgningen } return null; } catch (error) { Logger.log(` ⚠️ Søgefejl: ${error}`); return null; } } /** * SETUP: Opret automatisk daglig trigger * Kør denne funktion ÉN GANG for at sætte automatisering op */ function setupDailyTrigger() { // Fjern eksisterende triggers først const triggers = ScriptApp.getProjectTriggers(); triggers.forEach(trigger => { if (trigger.getHandlerFunction() === 'findAllTicketLinks') { ScriptApp.deleteTrigger(trigger); } }); // Opret ny trigger: Kør hver dag kl. 6:00 ScriptApp.newTrigger('findAllTicketLinks') .timeBased() .atHour(6) .everyDays(1) .create(); Logger.log("✅ Automatisk daglig trigger oprettet! Kører hver dag kl. 6:00"); } /** * Fjern automatisk trigger */ function removeDailyTrigger() { const triggers = ScriptApp.getProjectTriggers(); triggers.forEach(trigger => { if (trigger.getHandlerFunction() === 'findAllTicketLinks') { ScriptApp.deleteTrigger(trigger); Logger.log("✅ Automatisk trigger fjernet"); } }); } /** * TEST FUNKTION - Test på ét event */ function testTicketScraper() { Logger.log("🧪 ===== TEST BILLETLINK SCRAPER ====="); const testCases = [ { name: "Salsa Night", organizer: "Copenhagen Salsa Academy", venue: "The Old Irish Pub" }, { name: "Bachata Social", organizer: "Kizomba DK", venue: "CSA" }, { name: "Salsa Fridays", organizer: "Salsa Libre Copenhagen", venue: "Kedelhallen" } ]; testCases.forEach(test => { Logger.log(`\n🔍 Test: ${test.name} by ${test.organizer}`); const link = findTicketLink(test.name, test.organizer, test.venue); if (link) { Logger.log(`✅ Fandt: ${link}`); } else { Logger.log(`⚠️ Ingen link fundet`); } }); Logger.log("\n🎉 ===== TEST FÆRDIG ====="); } // ============================================ // 📋 SÅDAN BRUGER DU DET: // ============================================ // // 1. MANUEL TEST: // Kør funktionen: testTicketScraper() // For at teste på dummy data // // 2. FIND LINKS FOR ALLE EVENTS: // Kør funktionen: findAllTicketLinks() // Finder links for max 50 events uden links // // 3. AUTOMATISER (EN GANG): // Kør funktionen: setupDailyTrigger() // Sætter automatisk daglig kørsel op kl. 6:00 // // 4. STOP AUTOMATISERING: // Kør funktionen: removeDailyTrigger() // // ============================================ // ============================================ // 🧹 UTILITY FUNCTIONS // ============================================ /** * Clear Salsa.dk calendar cache * Kør denne når du vil have frisk data fra Salsa.dk */ function clearCache() { const cache = CacheService.getScriptCache(); cache.remove("salsa_calendar"); Logger.log("✅ Cache cleared! Næste request henter frisk data fra Salsa.dk"); } /** * Clear ALL caches (inkl. session data) */ function clearAllCaches() { const cache = CacheService.getScriptCache(); cache.removeAll(['salsa_calendar']); Logger.log("✅ Alle caches cleared!"); } /** * Force refresh - clear cache AND show what's scraped * Brug til debugging! */ function forceRefresh() { const cache = CacheService.getScriptCache(); cache.removeAll(['salsa_calendar']); Logger.log("✅ Cache cleared!"); // Hent ny data med det samme const events = getSalsaCalendarEvents(); Logger.log("📊 Fandt " + events.length + " events"); // Log første 10 event navne Logger.log("\n📋 Event navne:"); events.slice(0, 10).forEach((e, i) => { Logger.log(` ${i + 1}. ${e.Dansetype} - ${e.Sted}`); }); Logger.log("\n✅ Done! Test chatbot nu!"); } /** * Manually approve all events marked "Godkendt" * Run this function to move approved events to Events sheet */ function manualApprove() { const ss = SpreadsheetApp.openById(SHEET_ID); const pendingSheet = ss.getSheetByName("Pending"); const eventsSheet = ss.getSheetByName("Events"); if (!pendingSheet) { Logger.log("❌ Pending sheet not found!"); return; } if (!eventsSheet) { Logger.log("❌ Events sheet not found!"); return; } // Find all "Godkendt" rows const data = pendingSheet.getDataRange().getValues(); const headers = data[0]; const statusIndex = headers.indexOf('Status'); if (statusIndex === -1) { Logger.log("❌ Status column not found!"); return; } Logger.log(`📊 Found ${data.length - 1} rows in Pending`); Logger.log(`📍 Status column is at index ${statusIndex}`); let movedCount = 0; // Go through rows backwards (so deletion doesn't mess up indices) for (let i = data.length - 1; i > 0; i--) { const status = data[i][statusIndex]; Logger.log(`🔍 Row ${i + 1}: Status = "${status}"`); if (status === 'Godkendt') { Logger.log(`✅ Moving row ${i + 1}...`); // Copy to Events (remove Status column) const rowData = data[i].slice(); rowData.splice(statusIndex, 1); // Remove Status column eventsSheet.appendRow(rowData); // Delete from Pending pendingSheet.deleteRow(i + 1); movedCount++; Logger.log(`✅ Moved row ${i + 1} to Events!`); } } Logger.log(`\n🎉 Done! Moved ${movedCount} events to Events sheet!`); } // ============================================ // 📝 EVENT SUBMISSION SYSTEM // ============================================ /** * Try to parse user message as event submission and add to sheet */ function tryParseAndSubmitEvent(message) { try { const eventData = parseEventSubmission(message); if (!eventData) { return "🤔 Jeg kunne ikke forstå event informationen.\n\n" + "Prøv at inkludere:\n" + "• Dato (fx '6. december')\n" + "• Tidspunkt (fx '20:00')\n" + "• Type (Salsa/Bachata/Kizomba)\n" + "• Sted\n" + "• By\n\n" + "Eller skriv 'tilføj event' for vejledning! 😊"; } // Validate if (!eventData.Dato || !eventData.Dansetype || !eventData.Sted) { return "❌ Mangler information!\n\n" + "Jeg skal bruge minimum:\n" + "• Dato\n" + "• Type (Salsa/Bachata/Kizomba)\n" + "• Sted\n\n" + "Prøv igen! 😊"; } // Check for duplicates const duplicate = checkForDuplicate(eventData); if (duplicate) { return `⚠️ Dette event findes allerede!\n\n` + `📅 ${duplicate.Dansetype}\n` + `🕐 ${duplicate.Dato} ${duplicate.Starttid || ''}\n` + `📍 ${duplicate.Sted}` + (duplicate.By ? `, ${duplicate.By}` : '') + "\n\n" + `Tjek 'vis events' for at se det! 😊`; } // Add to Pending sheet const success = addEventToPending(eventData); if (success) { return "✅ **Event indsendt!**\n\n" + `📅 ${eventData.Dansetype}\n` + `🕐 ${eventData.Dato} ${eventData.Starttid || ''}\n` + `📍 ${eventData.Sted}` + (eventData.By ? `, ${eventData.By}` : '') + "\n\n" + "Tak for hjælpen! Eventet venter på godkendelse og vises snart! 🎉"; } else { return "❌ Noget gik galt ved tilføjelse af event.\n\n" + "Prøv igen eller kontakt support! 😊"; } } catch (error) { Logger.log("❌ Event submission error: " + error); return "❌ Der skete en fejl. Prøv igen! 😊"; } } /** * Parse event information from natural language */ function parseEventSubmission(message) { const msg = message.toLowerCase(); const eventData = {}; Logger.log("📝 Parsing submission: " + message); // Extract date - more flexible patterns const datePatterns = [ /(\d{1,2})\.?\s*(januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december)/i, /(\d{1,2})\.?\s*(jan|feb|mar|apr|maj|jun|jul|aug|sep|okt|nov|dec)/i, /(\d{4})-(\d{2})-(\d{2})/, /(\d{1,2})\/(\d{1,2})/ ]; for (const pattern of datePatterns) { const match = message.match(pattern); if (match) { if (pattern === datePatterns[0] || pattern === datePatterns[1]) { // Danish format: "6. december" or "6. dec" const day = match[1]; const monthMap = { 'januar': '01', 'jan': '01', 'februar': '02', 'feb': '02', 'marts': '03', 'mar': '03', 'april': '04', 'apr': '04', 'maj': '05', 'juni': '06', 'jun': '06', 'juli': '07', 'jul': '07', 'august': '08', 'aug': '08', 'september': '09', 'sep': '09', 'oktober': '10', 'okt': '10', 'november': '11', 'nov': '11', 'december': '12', 'dec': '12' }; const monthStr = match[2].toLowerCase(); const month = monthMap[monthStr]; // Check if year is mentioned const yearMatch = message.match(/202[4-9]/); const year = yearMatch ? yearMatch[0] : new Date().getFullYear(); eventData.Dato = `${year}-${month}-${day.padStart(2, '0')}`; Logger.log("✅ Dato parsed: " + eventData.Dato); } else if (pattern === datePatterns[2]) { // ISO format: "2024-12-06" eventData.Dato = match[0]; Logger.log("✅ Dato parsed (ISO): " + eventData.Dato); } break; } } // Extract time - more flexible const timeMatch = message.match(/(\d{1,2})[:\.-](\d{2})/); if (timeMatch) { eventData.Starttid = `${timeMatch[1].padStart(2, '0')}:${timeMatch[2]}`; Logger.log("✅ Tid parsed: " + eventData.Starttid); } // Extract dance type - check whole message if (msg.includes('salsa') && msg.includes('bachata')) { eventData.Dansetype = 'Salsa Bachata'; } else if (msg.includes('salsa')) { eventData.Dansetype = 'Salsa'; } else if (msg.includes('bachata')) { eventData.Dansetype = 'Bachata'; } else if (msg.includes('kizomba')) { eventData.Dansetype = 'Kizomba'; } if (eventData.Dansetype) { Logger.log("✅ Dansetype parsed: " + eventData.Dansetype); } // Extract venue/location - look for capitalized words or after commas const parts = message.split(/[,\n]/); for (const part of parts) { const trimmed = part.trim(); // Look for venue names (capitalized words) const venueMatch = trimmed.match(/\b([A-ZÆØÅ][a-zæøå]+(?:\s+[A-ZÆØÅ][a-zæøå]+)*)\b/); if (venueMatch && !eventData.Sted) { const candidate = venueMatch[0]; // Skip common words if (!['Salsa', 'Bachata', 'Kizomba', 'Type', 'Dato', 'Sted', 'By', 'Link', 'Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'].includes(candidate)) { eventData.Sted = candidate; Logger.log("✅ Sted parsed: " + eventData.Sted); break; } } } // Extract city const cities = ['københavn', 'kbh', 'aarhus', 'aalborg', 'odense', 'frederiksberg', 'nørdhavn', 'nørrebro', 'vesterbro']; for (const city of cities) { if (msg.includes(city)) { eventData.By = city.charAt(0).toUpperCase() + city.slice(1); if (city === 'kbh') eventData.By = 'København'; Logger.log("✅ By parsed: " + eventData.By); break; } } // Extract address - look for street patterns const addressMatch = message.match(/([A-ZÆØÅ][a-zæøå]+(?:gade|vej|allé|Boulevard|Plads|sgade|svej))\s*\d+[A-Z]?/i); if (addressMatch) { eventData.Adresse = addressMatch[0]; Logger.log("✅ Adresse parsed: " + eventData.Adresse); } // Extract link const linkMatch = message.match(/(https?:\/\/[^\s,]+)/i); if (linkMatch) { eventData.Link = linkMatch[1]; Logger.log("✅ Link parsed: " + eventData.Link); } Logger.log("📊 Final parsed data: " + JSON.stringify(eventData)); return eventData; } /** * Check if event already exists (duplicate detection) */ function checkForDuplicate(newEvent) { try { const ss = SpreadsheetApp.openById(SHEET_ID); const sheet = ss.getSheetByName("Events"); if (!sheet) return null; const data = sheet.getDataRange().getValues(); if (data.length <= 1) return null; // Only header const headers = data[0]; const datoIndex = headers.indexOf('Dato'); const stedIndex = headers.indexOf('Sted'); const tidIndex = headers.indexOf('Starttid'); // Normalize function for comparison const normalize = (str) => { if (!str) return ''; return String(str).toLowerCase().replace(/[^a-zæøå0-9]/g, ''); }; const newDate = newEvent.Dato ? newEvent.Dato.split(' ')[0] : ''; const newTime = newEvent.Starttid ? newEvent.Starttid.substring(0, 5) : ''; const newVenue = normalize(newEvent.Sted); // Check each existing event for (let i = 1; i < data.length; i++) { const row = data[i]; const existingDate = row[datoIndex] ? String(row[datoIndex]).split(' ')[0] : ''; const existingTime = row[tidIndex] ? String(row[tidIndex]).substring(0, 5) : ''; const existingVenue = normalize(row[stedIndex]); // Match if same date, similar time (within 30 min), and same venue if (existingDate === newDate && existingVenue === newVenue) { // Check time proximity if (newTime && existingTime) { const timeDiff = Math.abs( parseInt(newTime.split(':')[0]) * 60 + parseInt(newTime.split(':')[1]) - parseInt(existingTime.split(':')[0]) * 60 - parseInt(existingTime.split(':')[1]) ); if (timeDiff <= 30) { // It's a duplicate! let eventObj = {}; headers.forEach((header, idx) => { eventObj[header] = row[idx]; }); return eventObj; } } else { // No time info, just check date + venue let eventObj = {}; headers.forEach((header, idx) => { eventObj[header] = row[idx]; }); return eventObj; } } } return null; // No duplicate found } catch (error) { Logger.log("❌ Duplicate check error: " + error); return null; } } /** * Add event to Pending sheet (awaiting approval) */ function addEventToPending(eventData) { try { const ss = SpreadsheetApp.openById(SHEET_ID); let sheet = ss.getSheetByName(PENDING_SHEET_NAME); // Create Pending sheet if it doesn't exist if (!sheet) { sheet = createPendingSheet(ss); } // Get headers from PENDING sheet (not Events!) const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; Logger.log("📋 Pending headers: " + headers.join(", ")); // Create row data matching Pending headers const rowData = headers.map(header => { if (header === 'Status') return 'Pending'; if (header === 'source') return 'user_submitted'; return eventData[header] || ''; }); Logger.log("📝 Row data to add: " + JSON.stringify(rowData)); // Append to Pending sheet sheet.appendRow(rowData); Logger.log("✅ Event added to Pending: " + eventData.Dansetype + " at " + eventData.Sted); return true; } catch (error) { Logger.log("❌ Error adding to Pending: " + error); Logger.log("❌ Stack: " + error.stack); return false; } } /** * Create Pending sheet with proper structure */ function createPendingSheet(ss) { const sheet = ss.insertSheet(PENDING_SHEET_NAME); // Copy headers from Events sheet const eventsSheet = ss.getSheetByName("Events"); const headers = eventsSheet.getRange(1, 1, 1, eventsSheet.getLastColumn()).getValues()[0]; // Add Status column if not present if (!headers.includes('Status')) { headers.push('Status'); } sheet.getRange(1, 1, 1, headers.length).setValues([headers]); // Format header row sheet.getRange(1, 1, 1, headers.length) .setBackground('#4a90e2') .setFontColor('#ffffff') .setFontWeight('bold'); // Add dropdown for Status column const statusColumn = headers.indexOf('Status') + 1; const rule = SpreadsheetApp.newDataValidation() .requireValueInList(['Pending', 'Godkendt', 'Afvist']) .build(); sheet.getRange(2, statusColumn, 1000).setDataValidation(rule); Logger.log("✅ Created Pending sheet"); return sheet; } /** * Auto-approve events when Status changes to "Godkendt" * Install this as an onEdit trigger */ function onEdit(e) { try { // Handle case where e is undefined (manual trigger test) if (!e || !e.source) { Logger.log("⚠️ onEdit called without event object"); return; } const sheet = e.source.getActiveSheet(); // Only process Pending sheet if (sheet.getName() !== PENDING_SHEET_NAME) return; const range = e.range; const row = range.getRow(); const col = range.getColumn(); // Check if Status column was edited const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; const statusCol = headers.indexOf('Status') + 1; if (col === statusCol && row > 1) { const newStatus = range.getValue(); Logger.log(`📝 Status changed to: ${newStatus} in row ${row}`); if (newStatus === 'Godkendt') { // Move to Events sheet moveToEvents(sheet, row); Logger.log("✅ Event godkendt og flyttet!"); } else if (newStatus === 'Afvist') { // Delete row sheet.deleteRow(row); Logger.log("❌ Event afvist og slettet fra række " + row); } } } catch (error) { Logger.log("❌ onEdit error: " + error); Logger.log("❌ Stack: " + error.stack); } } /** * Move event from Pending to Events sheet */ function moveToEvents(pendingSheet, row) { try { const ss = SpreadsheetApp.openById(SHEET_ID); const eventsSheet = ss.getSheetByName("Events"); // Get data from Pending sheet const rowData = pendingSheet.getRange(row, 1, 1, pendingSheet.getLastColumn()).getValues()[0]; // Remove Status column data before adding to Events const headers = pendingSheet.getRange(1, 1, 1, pendingSheet.getLastColumn()).getValues()[0]; const statusIndex = headers.indexOf('Status'); if (statusIndex !== -1) { rowData.splice(statusIndex, 1); } // Add to Events sheet eventsSheet.appendRow(rowData); // Delete from Pending pendingSheet.deleteRow(row); Logger.log("✅ Event godkendt og flyttet til Events sheet"); } catch (error) { Logger.log("❌ Move to Events error: " + error); } } /** * DEBUG FUNCTION - Test submission parsing */ function debugSubmission() { Logger.log("🧪 Test started"); const testMessage = "10. december, 20:00, Salsa, Kedelhallen, København"; const result = tryParseAndSubmitEvent(testMessage); Logger.log("📝 Result: " + result); }