fbpx
// VARIABILI GLOBALI const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; const ID_INIZIALE_NUMERICO = 260776; // Aggiornato per ripartire dopo DA260775 const ID_PREFIX = 'DA'; // Prefisso fisso per le nuove prenotazioni /** * Funzione di servizio principale che gestisce le richieste GET e POST dal modulo HTML esterno. * @param {Object} e - Event object contenente i dati della richiesta. */ function doGet(e) { const params = e.parameter; const action = params.action; if (action === 'getMenus') { const dateString = params.date; // Data nel formato YYYY-MM-DD const tipo = params.type; // 'cena' o 'dopocena' // Converte YYYY-MM-DD in GG/MM/AAAA per la logica interna const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; try { const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })) .setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore nel recupero dei dati: ' + error.message })) .setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio attivo. Invia i dati tramite POST.'); } /** * Funzione di servizio per gestire l'invio del modulo (POST request) */ // ... (altre variabili globali) // ... /** * Funzione di servizio per gestire l'invio del modulo (POST request) */ function doPost(e) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); let data; try { data = JSON.parse(e.postData.contents); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati POST non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // --- GESTIONE ID PROGRESSIVO --- const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { if (ultimaRiga === 0) { // !!! RIGA AGGIORNATA PER INCLUDERE LE NUOVE INTESTAZIONI !!! sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDString = String(sheet.getRange('A' + ultimaRiga).getValue()); const match = ultimoIDString.match(/\d+$/); const ultimoIDValue = match ? parseInt(match[0], 10) : 0; nuovoID_Numerico = (ultimoIDValue >= ID_INIZIALE_NUMERICO) ? ultimoIDValue + 1 : ID_INIZIALE_NUMERICO; } const nuovoID = ID_PREFIX + nuovoID_Numerico; // --- FINE GESTIONE ID --- // Mappatura dati e calcoli // ... (calcoli omessi per brevità) const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const sconto = 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); const tipoPrenotazione = data.source === 'DA' ? 'Cena' : (data.source === 'DOPOCENA' ? 'Dopocena' : data.source); // !!! COLONNA Q (LINK STRIPE) IMPOSTATA SU PLACEHOLDER PER ELABORAZIONE ASINCRONA !!! const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; // NUOVI CAMPI (Se non inviati dal form, saranno stringhe vuote) const descrizioneMenu = data.descrizioneMenu || ''; const descrizioneAnimazione = data.descrizioneAnimazione || ''; const rigaDaScrivere = [ nuovoID, // A: ID dataCorrente, // B: Data Creazione dataCliente, // C: Data Campo Cliente data.nome, // D: Nome data.telefono, // E: Telefono parseInt(data.numClienti, 10), // F: N° Clienti data.pacchetto, // G: Menu costoAnimazione, // H: Animazione (Costo) 0, // I: Extra (0) data.note, // J: Note data.richiediAcconto, // K: Richiedi Acconto ('Si') totale, // L: Totale sconto, // M: Sconto (0) importoAcconto, // N: Importo acconto totDaPagare, // O: Tot. Da Pagare 'Acconto da pagare', // P: Stato acconto (Iniziale) stripeLinkPlaceholder, // Q: Link Stripe (Placeholder) 'In attesa di invio', // R: Stato automazione '', // S: Email cliente nuovoID_Numerico, // T: ID_Numerico tipoPrenotazione, // U: Tipo Prenotazione descrizioneMenu, // V: Descrizione Menu (!!! NUOVO !!!) descrizioneAnimazione // W: Descrizione Animazione (!!! NUOVO !!!) ]; sheet.appendRow(rigaDaScrivere); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione...' })).setMimeType(ContentService.MimeType.JSON); } // ... (il resto di Codice.gs) // ------------------------------------------------------------------------------------------------ // FUNZIONI DI SUPPORTO PER RECUPERO DATI (GET) // ------------------------------------------------------------------------------------------------ function getMenuPackages(dateString, tipo) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const dateObj = new Date(parts[2], parts[1] - 1, parts[0]); const dayOfWeek = dateObj.getDay(); function formatDateForComparison(date) { if (Object.prototype.toString.call(date) === '[object Date]') { const d = date.getDate(); const m = date.getMonth() + 1; const y = date.getFullYear(); return `${d}/${m}/${y}`; } return String(date); } // 1. PRIORITÀ AL FILTRO PER DATA ESATTA (Colonna B) const filteredByDate = data.filter(row => { const sheetTipo = String(row[0]).toLowerCase(); const sheetDate = formatDateForComparison(row[1]); const inputDateClean = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/').replace(/^\//, ''); return sheetTipo === tipo.toLowerCase() && sheetDate === inputDateClean; }); if (filteredByDate.length > 0) { const menusByDate = filteredByDate.map(row => ({ value: `${row[2]} (€ ${row[4].toFixed(2).replace('.', ',')})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' })); return menusByDate; } // 2. FALLBACK ALLA LOGICA FERIALE / WEEKEND / LUNEDÌ let tipoGiorno = ''; if (dayOfWeek === 1) { tipoGiorno = 'Lunedì'; } else if (dayOfWeek >= 2 && dayOfWeek <= 4 || dayOfWeek === 0) { tipoGiorno = 'Feriale'; } else if (dayOfWeek === 5 || dayOfWeek === 6) { tipoGiorno = 'Weekend'; } // Logica Lunedì Chiusi if (tipoGiorno === 'Lunedì') { const lunediChiusi = data.filter(row => String(row[0]).toLowerCase() === tipo.toLowerCase() && String(row[1]) === 'Lunedì' && String(row[2]).includes('Lunedì chiusi')); if (lunediChiusi.length > 0) { return [{ value: `CHIUSO: ${lunediChiusi[0][2]}`, description: lunediChiusi[0][3], price: 0, sexyCamAllowed: false }]; } } // Filtra per Tipo e Giorno const filteredMenus = data.filter(row => { const sheetTipo = String(row[0]).toLowerCase(); const matchTipo = sheetTipo === tipo.toLowerCase(); const matchGiorno = String(row[1]) === tipoGiorno; if (['Feriale', 'Weekend', 'Lunedì'].includes(String(row[1]))) { return matchTipo && matchGiorno; } return false; }).map(row => ({ value: `${row[2]} (€ ${row[4].toFixed(2).replace('.', ',')})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' })); if (filteredMenus.length === 0) { return [{ value: 'Nessun Menù Disponibile', description: 'Non ci sono pacchetti attivi per la data selezionata.', price: 0, sexyCamAllowed: false }]; } return filteredMenus; } // ... (Nel tuo Codice.gs) function getAnimationPackages() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_ANIMAZIONE); // Assumendo che la Colonna C (indice 2) contenga la Spiegazione const data = sheet.getDataRange().getValues().slice(1); return data.map(row => ({ value: `${row[0]} (€ ${row[1].toFixed(2).replace('.', ',')})`, price: row[1], description: row[2] // ⬅️ ORA RECUPERA LA SPIEGAZIONE DALLA COLONNA C (indice 2) })).filter(item => item.price > 0); } // ... (tutto il resto del codice di Codice.gs)
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI UTENTE const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; const ID_INIZIALE_NUMERICO = 260776; const ID_PREFIX = 'DA'; // VARIABILI STRIPE // Chiave con permessi di Scrittura corretti const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; // *** CHIAVE SEGRETA WEBHOOK (whsec_) - INSERITA QUI *** const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; // A: ID Prenotazione const COL_PACCHETTO = 6; // G: Menu (Pacchetto) const COL_ACCONTO = 13; // N: Importo Acconto const COL_STATO = 15; // P: Stato Acconto const COL_LINK_STRIPE = 16; // Q: Link Stripe // ========================================================= // --- 2. FUNZIONI PRINCIPALI (doGet, doPost) --- // ========================================================= /** * Funzione di servizio principale che gestisce le richieste GET dal modulo HTML. */ function doGet(e) { const params = e.parameter; const action = params.action; if (action === 'getMenus') { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; try { const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })) .setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore nel recupero dei dati: ' + error.message })) .setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio attivo. Invia i dati tramite POST.'); } /** * Funzione unificata che gestisce sia l'invio del modulo sia i Webhook di Stripe. */ function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { // Questo potrebbe essere un Webhook raw non JSON, lo gestiamo nel blocco try/catch successivo // Se il parsing JSON fallisce, assumiamo sia una richiesta Webhook con body non standard // Tentiamo di verificare se è un Webhook Stripe valido if (e.postData.contents && e.parameter.event_type) { // Controllo rapido per Webhook Logger.log('Ricevuta richiesta POST non JSON, assumendo sia Webhook Stripe non standard.'); return handleStripeWebhook(e); } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati POST non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // --- 1. GESTIONE WEBHOOK STRIPE (Aggiornamento stato) --- // Se il payload ha la proprietà 'type', è un evento Stripe. if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); Logger.log('Webhook ricevuto e riga aggiornata per ID: ' + clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // --- 2. GESTIONE INVIO MODULO --- const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // --- GESTIONE ID PROGRESSIVO --- const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { if (ultimaRiga === 0) { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDString = String(sheet.getRange('A' + ultimaRiga).getValue()); const match = ultimoIDString.match(/\d+$/); const ultimoIDValue = match ? parseInt(match[0], 10) : 0; nuovoID_Numerico = (ultimoIDValue >= ID_INIZIALE_NUMERICO) ? ultimoIDValue + 1 : ID_INIZIALE_NUMERICO; } const nuovoID = ID_PREFIX + nuovoID_Numerico; // Mappatura dati e calcoli const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const sconto = 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); const tipoPrenotazione = data.source === 'DA' ? 'Cena' : (data.source === 'DOPOCENA' ? 'Dopocena' : data.source); // Colonna Q (LINK STRIPE) IMPOSTATA SU PLACEHOLDER const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; const descrizioneMenu = data.descrizioneMenu || ''; const descrizioneAnimazione = data.descrizioneAnimazione || ''; const rigaDaScrivere = [ nuovoID, // A: ID dataCorrente, // B: Data Creazione dataCliente, // C: Data Campo Cliente data.nome, // D: Nome data.telefono, // E: Telefono parseInt(data.numClienti, 10), // F: N° Clienti data.pacchetto, // G: Menu costoAnimazione, // H: Animazione (Costo) 0, // I: Extra (0) data.note, // J: Note data.richiediAcconto, // K: Richiedi Acconto ('Si') totale, // L: Totale sconto, // M: Sconto (0) importoAcconto, // N: Importo acconto totDaPagare, // O: Tot. Da Pagare 'Acconto da pagare', // P: Stato acconto (Iniziale) stripeLinkPlaceholder, // Q: Link Stripe (Placeholder) 'In attesa di invio', // R: Stato automazione '', // S: Email cliente nuovoID_Numerico, // T: ID_Numerico tipoPrenotazione, // U: Tipo Prenotazione descrizioneMenu, // V: Descrizione Menu descrizioneAnimazione // W: Descrizione Animazione ]; sheet.appendRow(rigaDaScrivere); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Il link Stripe verrà generato automaticamente a breve.' })).setMimeType(ContentService.MimeType.JSON); } // =================================== // --- 3. FUNZIONI STRIPE (LINK E STATO) --- // =================================== function convertObjectToUrlEncodedString(obj) { return Object.keys(obj) .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])) .join('&'); } /** * Funzione per la creazione del link Stripe. */ function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto da pagare.'; const amountInCents = Math.round(numericAmount * 100); // 1. CREAZIONE PRICE (e Prodotto) const productName = `Acconto Prenotazione ${bookingId} - ${packageName}`; const pricePayload = { 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, 'expand[]': 'product', 'recurring[interval]': 'month', 'recurring[interval_count]': 1, 'billing_scheme': 'per_unit' }; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString(pricePayload), 'muteHttpExceptions': true }; let priceId = ''; try { const priceResponse = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const priceJson = JSON.parse(priceResponse.getContentText()); if (priceJson.id) { priceId = priceJson.id; } else { Logger.log('Errore API Stripe (Prezzo): ' + JSON.stringify(priceJson)); return 'ERRORE STRIPE (Prezzo): ' + JSON.stringify(priceJson.error).substring(0, 50) + '...'; } } catch (e) { return 'ERRORE DI RETE/API (Prezzo): ' + e.toString().substring(0, 50) + '...'; } // 2. CREAZIONE PAYMENT LINK const linkPayload = { 'line_items[0][price]': priceId, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId, }; try { const linkResponse = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString(linkPayload), muteHttpExceptions: true }); const linkJson = JSON.parse(linkResponse.getContentText()); if (linkJson.url) { return linkJson.url; } else { Logger.log('Errore API Stripe (Link): ' + JSON.stringify(linkJson)); return 'ERRORE STRIPE (Link): ' + JSON.stringify(linkJson.error).substring(0, 50) + '...'; } } catch (e) { return 'ERRORE DI RETE/API (Link): ' + e.toString().substring(0, 50) + '...'; } } /** * Funzione da eseguire con un trigger orario per creare i link mancanti. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { const row = values[i]; const stripeLink = row[COL_LINK_STRIPE]; const importoAcconto = row[COL_ACCONTO]; const bookingId = row[COL_ID]; const packageName = row[COL_PACCHETTO]; // Criterio: Colonna Q è 'LINK_STRIPE_TEMP' (Placeholder) E acconto > 0 if (stripeLink === 'LINK_STRIPE_TEMP' && importoAcconto > 0) { Logger.log(`Trovata riga ${i + 1} da processare.`); const newLink = createStripePaymentLink(importoAcconto, bookingId, packageName); if (newLink && newLink.startsWith('http')) { sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); Logger.log(`Link creato per ID ${bookingId}.`); } else { sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE'); Logger.log(`FALLIMENTO creazione link per ID ${bookingId}: ${newLink}`); } } } } /** * Cerca l'ID prenotazione e aggiorna lo stato a 'ACCONTO PAGATO'. */ function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { // Cerchiamo l'ID nella Colonna A (COL_ID) if (data[i][COL_ID] == bookingId) { // Aggiorna lo stato in Colonna P (COL_STATO) sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } // =================================== // --- 4. FUNZIONI DI SUPPORTO DATI (GET) --- // =================================== function getMenuPackages(dateString, tipo) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const dateObj = new Date(parts[2], parts[1] - 1, parts[0]); const dayOfWeek = dateObj.getDay(); function formatDateForComparison(date) { if (Object.prototype.toString.call(date) === '[object Date]') { const d = date.getDate(); const m = date.getMonth() + 1; const y = date.getFullYear(); return `${d}/${m}/${y}`; } return String(date); } // 1. PRIORITÀ AL FILTRO PER DATA ESATTA (Colonna B) const filteredByDate = data.filter(row => { const sheetTipo = String(row[0]).toLowerCase(); const sheetDate = formatDateForComparison(row[1]); const inputDateClean = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/').replace(/^\//, ''); return sheetTipo === tipo.toLowerCase() && sheetDate === inputDateClean; }); if (filteredByDate.length > 0) { const menusByDate = filteredByDate.map(row => ({ value: `${row[2]} (€ ${row[4].toFixed(2).replace('.', ',')})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' })); return menusByDate; } // 2. FALLBACK ALLA LOGICA FERIALE / WEEKEND / LUNEDÌ let tipoGiorno = ''; if (dayOfWeek === 1) { tipoGiorno = 'Lunedì'; } else if (dayOfWeek >= 2 && dayOfWeek <= 4 || dayOfWeek === 0) { tipoGiorno = 'Feriale'; } else if (dayOfWeek === 5 || dayOfWeek === 6) { tipoGiorno = 'Weekend'; } // Logica Lunedì Chiusi if (tipoGiorno === 'Lunedì') { const lunediChiusi = data.filter(row => String(row[0]).toLowerCase() === tipo.toLowerCase() && String(row[1]) === 'Lunedì' && String(row[2]).includes('Lunedì chiusi')); if (lunediChiusi.length > 0) { return [{ value: `CHIUSO: ${lunediChiusi[0][2]}`, description: lunediChiusi[0][3], price: 0, sexyCamAllowed: false }]; } } // Filtra per Tipo e Giorno const filteredMenus = data.filter(row => { const sheetTipo = String(row[0]).toLowerCase(); const matchTipo = sheetTipo === tipo.toLowerCase(); const matchGiorno = String(row[1]) === tipoGiorno; if (['Feriale', 'Weekend', 'Lunedì'].includes(String(row[1]))) { return matchTipo && matchGiorno; } return false; }).map(row => ({ value: `${row[2]} (€ ${row[4].toFixed(2).replace('.', ',')})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' })); if (filteredMenus.length === 0) { return [{ value: 'Nessun Menù Disponibile', description: 'Non ci sono pacchetti attivi per la data selezionata.', price: 0, sexyCamAllowed: false }]; } return filteredMenus; } function getAnimationPackages() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_ANIMAZIONE); const data = sheet.getDataRange().getValues().slice(1); return data.map(row => ({ value: `${row[0]} (€ ${row[1].toFixed(2).replace('.', ',')})`, price: row[1], description: row[2] })).filter(item => item.price > 0); }
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI UTENTE const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; // A: ID const COL_PACCHETTO = 6; // G: Menu const COL_ACCONTO = 13; // N: Acconto const COL_STATO = 15; // P: Stato const COL_LINK_STRIPE = 16; // Q: Link // ========================================================= // --- 2. FUNZIONI PRINCIPALI (doGet, doPost) --- // ========================================================= function doGet(e) { const params = e.parameter; if (params.action === 'getMenus') { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })).setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore: ' + error.message })).setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio attivo.'); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { return handleStripeWebhook(e); // Gestione webhook raw } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; // 1. Logica per Inizializzazione o Intestazione (Righe 0 o 1) if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { // Appende l'intestazione se manca sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } // Imposta l'ID numerico iniziale nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { // 2. Logica per Incremento (CERCA IL MASSIMO ID IN TUTTA LA COLONNA SE L'ULTIMO È ERRATO) // Leggi l'ultimo numero dalla colonna T (ID_NUMERICO_COL + 1 = 20) const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; // Se l'ultimo ID letto è 0, NaN o minore dell'ID iniziale, eseguiamo la ricerca robusta if (ultimoID < ID_INIZIALE_NUMERICO) { // Carica tutti i valori numerici dalla Colonna T (dal rigo 2 all'ultima riga) const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } // Calcola il nuovo ID dal massimo trovato nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { // Caso normale: l'ID sulla riga precedente è valido, lo incrementiamo nuovoID_Numerico = ultimoID + 1; } } // 3. Determinazione del Prefisso e Assemblaggio const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // Mappa il prefisso al tipo di prenotazione let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; // ARRAY DATI RIGA CORRETTO con Descrizione Menu (V) e Descrizione Animazione (W) const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, 'In attesa di invio', '', nuovoID_Numerico, tipoPrenotazione, // ID_Numerico in T data.descrizioneMenu || '', // Colonna V data.descrizioneAnimazione || ''// Colonna W ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione.' })).setMimeType(ContentService.MimeType.JSON); } // =================================== // --- 3. FUNZIONI STRIPE & EXTRA --- // =================================== function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } /** * MODIFICATO: Rimosse le opzioni 'recurring' e aggiornato il nome del prodotto (SOLO ID). */ function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); // NOME PRODOTTO SEMPLIFICATO COME RICHIESTO: solo "Acconto Prenotazione DAXXXXXX" const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, // Rimosse le opzioni recurring per un pagamento una tantum }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione che scansiona il foglio e genera i link Stripe. * Deve essere eseguita tramite un Trigger a Tempo. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { // Cerca il placeholder "LINK_STRIPE_TEMP" e controlla che l'acconto sia maggiore di zero if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); // Scrive il link o l'errore sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); // Aggiorna lo stato in Colonna P (COL_STATO) sheet.getRange(i + 1, COL_STATO + 1).setValue(newLink.startsWith('http') ? 'Link Stripe Generato' : 'ERRORE STRIPE nella creazione del link'); } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); // Logica semplificata date const filtered = data.filter(row => { const d = row[1]; // Assumendo sia oggetto data const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); // Fallback giorni (semplificato per brevità, logica completa mantenuta se necessario espandere) const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; // Sab/Dom // Controllo Lunedì if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); }
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI FOGLIO const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; const FOGLIO_MESSAGGI = 'Messaggi'; // VARIABILI ID / CONTATORI const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; const COL_DATA_PRENOTAZIONE = 2; const COL_NOME = 3; const COL_TELEFONO = 4; const COL_NUM_CLIENTI = 5; const COL_PACCHETTO = 6; const COL_ANIMAZIONE = 7; const COL_ACCONTO = 13; const COL_SALDO = 14; const COL_STATO = 15; const COL_LINK_STRIPE = 16; const COL_STATO_AUTOMAZIONE = 17; // Stato per l'automazione WhatsApp/Stripe // VARIABILI WHATSAPP (SENDAPP) const SENDAPP_API_URL = 'https://app.sendapp.cloud/api/send'; const SENDAPP_TOKEN = '68863b87201fc'; const SENDAPP_INSTANCE_ID = '68863C08859E7'; const MESSAGGI_KEY_COL = 0; const MESSAGGI_CONTENT_COL = 1; const MESSAGGIO1_KEY = 'Messaggio1'; // Con link Acconto const MESSAGGIO2_KEY = 'Messaggio2'; // Senza link Acconto // STATI DI AUTOMAZIONE const STATO_IN_ATTESA = 'In attesa di invio'; const STATO_LINK_GENERATO = 'Link Generato'; const STATO_PRONTO_PER_INVIO = 'Pronto per Invio'; // Per Acconto zero const STATO_INVIATO = 'Inviato'; const STATO_ERRORE_INVIO = 'Errore Invio'; // ========================================================= // --- 2. FUNZIONI PRINCIPALI (doGet, doPost) --- // ========================================================= function doGet(e) { const params = e.parameter; if (params.action === 'getMenus') { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })).setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore: ' + error.message })).setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio attivo.'); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { return handleStripeWebhook(e); // Da implementare se non lo è } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } // Se l'acconto è richiesto, si usa il placeholder per poi generare il link const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; // Stato iniziale per la colonna R: in attesa della creazione del link o pronto se acconto è zero const statoInizialeWhatsApp = (importoAcconto > 0) ? STATO_IN_ATTESA : STATO_PRONTO_PER_INVIO; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, statoInizialeWhatsApp, // COL R: Stato Automazione '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '' ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione (se richiesto).' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 3. FUNZIONI STRIPE & MENU --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione da eseguire con trigger per generare i link Stripe. * Aggiorna lo stato di automazione se il link è stato creato. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { const statoAutomazioneCorrente = String(values[i][COL_STATO_AUTOMAZIONE]).trim(); // Controlla se è necessario generare il link if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); // Aggiorna lo stato per l'invio WhatsApp if (statoAutomazioneCorrente === STATO_IN_ATTESA) { sheet.getRange(i + 1, COL_STATO_AUTOMAZIONE + 1).setValue(STATO_LINK_GENERATO); } } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE nella creazione del link'); } } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); } // ========================================================= // --- 4. FUNZIONI WHATSAPP (SENDAPP) --- // ========================================================= /** * Legge il template del messaggio dal foglio 'Messaggi'. */ function getWhatsAppTemplate(key) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_MESSAGGI); if (!sheet) { Logger.log(`ERRORE: Foglio '${FOGLIO_MESSAGGI}' non trovato.`); return `ERRORE: Template non trovato per ${key}.`; } const values = sheet.getDataRange().getValues(); for (let i = 0; i < values.length; i++) { if (String(values[i][MESSAGGI_KEY_COL]).trim() === key) { return String(values[i][MESSAGGI_CONTENT_COL]); } } Logger.log(`ERRORE: Chiave template '${key}' non trovata nel foglio 'Messaggi'.`); return `ERRORE: Template non trovato per ${key}.`; } /** * Sostituisce i placeholder {...} nel template con i dati della prenotazione. * 👉 Funzione Modificata per supportare {placeholder} in minuscolo. */ function replacePlaceholders(template, data) { let message = template; // Mappatura dei placeholder con le chiavi in minuscolo e parentesi singole const placeholderMap = { '{nome_cliente}': data.nomeCliente, '{id_prenotazione}': data.bookingId, '{data_evento}': data.dataPrenotazione, '{numero_persone}': data.numClienti, '{pacchetto_scelto}': data.nomePacchetto, '{acconto_importo}': data.importoAccontoFormatted, '{saldo_importo}': data.saldoFormatted, '{link_stripe}': data.linkStripe }; for (const placeholder in placeholderMap) { const value = placeholderMap[placeholder]; // Sostituisce la chiave, assicurandosi di fare l'escape delle parentesi graffe per RegExp message = message.replace(new RegExp(placeholder.replace(/([{}])/g, "\\$1"), 'g'), value); } // Gestione specifica dell'animazione let animazioneText = ''; if (data.animazioneScelta && String(data.animazioneScelta).length > 0 && parseFloat(data.animazioneScelta) !== 0) { animazioneText = `Animazione scelta: ${data.animazioneScelta}`; } // Sostituzione specifica per l'animazione (utilizzando lo stesso stile per coerenza) message = message.replace(/{animazione_scelta}/g, animazioneText); return message; } /** * Funzione di utilità per inviare il messaggio tramite API SendApp. */ function sendWhatsAppMessage(recipient, message) { if (!SENDAPP_API_URL || !SENDAPP_TOKEN || !SENDAPP_INSTANCE_ID) { Logger.log('Errore: Credenziali API SendApp non configurate correttamente.'); return false; } const apiUrl = SENDAPP_API_URL; const payload = { "access_token": SENDAPP_TOKEN, "instance_id": SENDAPP_INSTANCE_ID, "number": recipient, "message": message }; const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; try { const response = UrlFetchApp.fetch(apiUrl, options); const result = JSON.parse(response.getContentText()); Logger.log('Risposta API WhatsApp per ' + recipient + ': ' + JSON.stringify(result)); if (result.status && result.status === 'success') { return true; } else { Logger.log('Errore nell\'invio WhatsApp: ' + (result.message || 'Errore sconosciuto API.')); return false; } } catch (e) { Logger.log('Errore di connessione API WhatsApp: ' + e.toString()); return false; } } /** * Funzione eseguita dal trigger a tempo (es. ogni minuto) per inviare i messaggi. */ function processPendingMessages() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); const lastRow = sheet.getLastRow(); if (lastRow <= 1) return; const range = sheet.getRange(2, 1, lastRow - 1, 23); const values = range.getValues(); for (let i = 0; i < values.length; i++) { const data = values[i]; const currentRow = i + 2; const statoAutomazione = String(data[COL_STATO_AUTOMAZIONE]).trim(); // Controlla se la riga è pronta per l'invio (dopo Stripe o acconto zero) if (statoAutomazione !== STATO_LINK_GENERATO && statoAutomazione !== STATO_PRONTO_PER_INVIO) { continue; } // 1. Estrazione Dati const bookingId = data[COL_ID]; let telefono = data[COL_TELEFONO]; const nomeCliente = data[COL_NOME]; const dataPrenotazioneRaw = data[COL_DATA_PRENOTAZIONE]; const dataPrenotazione = dataPrenotazioneRaw instanceof Date ? Utilities.formatDate(dataPrenotazioneRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenotazioneRaw); const numClienti = data[COL_NUM_CLIENTI]; const nomePacchetto = data[COL_PACCHETTO]; const animazioneScelta = data[COL_ANIMAZIONE]; const importoAcconto = data[COL_ACCONTO]; const saldo = data[COL_SALDO]; const linkStripe = data[COL_LINK_STRIPE]; // 2. Pulizia e formattazione rigorosa del numero di telefono (FIX: Aggiunge +39 se necessario) telefono = String(telefono).replace(/[^\d\+]/g, ''); // Rimuovi eventuali + o 00 iniziali if (telefono.startsWith('+')) { telefono = telefono.substring(1); } else if (telefono.startsWith('00')) { telefono = telefono.substring(2); } // Aggiungi 39 se non è presente if (!telefono.startsWith('39')) { // Rimuovi eventuali zeri iniziali di troppo e aggiungi il prefisso 39 telefono = '39' + telefono.replace(/^0*/, ''); } // Assicurati che rimangano solo cifre per l'invio telefono = telefono.replace(/[^\d]/g, ''); // 3. Preparazione dei dati per il placeholder const accontoValue = parseFloat(importoAcconto) || 0; const isAccontoRequired = (accontoValue > 0); const payloadData = { nomeCliente: nomeCliente, bookingId: bookingId, dataPrenotazione: dataPrenotazione, numClienti: numClienti, nomePacchetto: nomePacchetto, animazioneScelta: animazioneScelta, importoAccontoFormatted: accontoValue.toFixed(2).replace('.', ','), saldoFormatted: (parseFloat(saldo) || 0).toFixed(2).replace('.', ','), linkStripe: linkStripe }; // 4. LOGICA DI SCELTA DEL MESSAGGIO let messageKey = isAccontoRequired ? MESSAGGIO1_KEY : MESSAGGIO2_KEY; // 5. INVIO e AGGIORNAMENTO STATO const rawTemplate = getWhatsAppTemplate(messageKey); const finalMessage = replacePlaceholders(rawTemplate, payloadData); const isSent = sendWhatsAppMessage(telefono, finalMessage); const statoColIndex = COL_STATO_AUTOMAZIONE + 1; const statoCell = sheet.getRange(currentRow, statoColIndex); if (isSent) { statoCell.setValue(STATO_INVIATO); } else { // Mantiene lo stato pronto, ma registra l'errore per il debug statoCell.setValue(STATO_ERRORE_INVIO + ` (${statoAutomazione})`); Logger.log(`Tentativo fallito per ID ${bookingId}, telefono ${telefono}.`); } } }
// ========================================================= // === FILE: Codice.gs === // (Utilizza costanti da CostantiGlobali.gs) // ========================================================= // ========================================================= // --- 2. FUNZIONI PRINCIPALI (doGet, doPost) --- // ========================================================= function doGet(e) { const params = e.parameter; if (params.action === 'getMenus') { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })).setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore: ' + error.message })).setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio attivo.'); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { // Questa sezione originale non funzionerà correttamente per la validazione Stripe. // Se si attiva, dovrebbe essere gestita altrove. Manteniamo la struttura per ora. // Se non hai la funzione handleStripeWebhook, il webhook fallirà in questa linea. return handleStripeWebhook(e); } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE (omesso per brevità, resta invariato) if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; // LOGICA ID: if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione', 'Stato Staff']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } // Se l'acconto è richiesto, si usa il placeholder per poi generare il link const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; // Stato iniziale per la colonna R: in attesa della creazione del link o pronto se acconto è zero const statoInizialeWhatsApp = (importoAcconto > 0) ? STATO_IN_ATTESA : STATO_PRONTO_PER_INVIO; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, statoInizialeWhatsApp, // COL R: Stato Automazione Cliente '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '', // Colonna V e W '' // Colonna X: Stato Staff ]; // === SALVATAGGIO RIGA === sheet.appendRow(riga); // === LOGICA: NOTIFICA IMMEDIATA STAFF (Messaggio5) con Diagnosi === try { const numeriDirettore = getNumeriDirettore(); // Funzione nel file AutomazioneStaff.gs if (numeriDirettore.length === 0) { Logger.log('AVVISO: Nessun Direttore trovato per inviare Messaggio5.'); // === STRUMENTO DI DIAGNOSI: Invia una notifica di fallimento a un numero di test === // ⚠️ INSERISCI QUI IL TUO NUMERO DI CELLULARE PERSONALE (formato 39xxxxxxxxxx) const numeroTestDiagnosi = '39xxxxxxxxxx'; const messaggioDiagnosi = `ERRORE CRITICO: La funzione getNumeriDirettore() ha trovato 0 numeri. Controllare il foglio Staff (Mansione: DIRETTORE). ID Prenotazione: ${nuovoID}`; sendWhatsAppMessage(numeroTestDiagnosi, messaggioDiagnosi); // ================================================================================= } else { // Logica di invio Messaggio5 se i numeri sono stati trovati const template5 = getWhatsAppTemplate(MESSAGGIO_NOTIFICA_DIRETTORE); const payloadNotifica = { nomeCliente: data.nome, bookingId: nuovoID, dataPrenotazione: Utilities.formatDate(dataCliente, Session.getScriptTimeZone(), 'dd/MM/yyyy'), numClienti: data.numClienti, pacchettoScelto: data.pacchetto, accontoImportoFormatted: importoAcconto.toFixed(2).replace('.', ','), tipoPrenotazione: tipoPrenotazione, note: data.note || '' }; const messaggio5 = replacePlaceholders(template5, payloadNotifica); numeriDirettore.forEach(numero => { sendWhatsAppMessage(numero, messaggio5); }); Logger.log(`Messaggio5 (Nuova Prenotazione) inviato a ${numeriDirettore.length} Direttori per ID ${nuovoID}`); } } catch(e) { Logger.log(`ERRORE nell'invio Messaggio5 al Direttore per ID ${nuovoID}: ${e.message}`); } return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione (se richiesto).' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 3. FUNZIONI STRIPE & MENU --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione da eseguire con trigger per generare i link Stripe. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { const statoAutomazioneCorrente = String(values[i][COL_STATO_AUTOMAZIONE]).trim(); // Controlla se è necessario generare il link if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); // Aggiorna lo stato per l'invio WhatsApp if (statoAutomazioneCorrente === STATO_IN_ATTESA) { sheet.getRange(i + 1, COL_STATO_AUTOMAZIONE + 1).setValue(STATO_LINK_GENERATO); } } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE nella creazione del link'); } } } } /** * Funzione richiamata dal webhook Stripe o manualmente al pagamento dell'acconto. * Ora include l'invio del Messaggio6 al Cassiere. */ function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { const currentRow = i + 1; // 1. Aggiorna lo stato nel foglio Prenotazioni sheet.getRange(currentRow, COL_STATO + 1).setValue('ACCONTO PAGATO'); // 2. Logica di invio Messaggio6 al Cassiere try { // Estrai i dati necessari dalla riga per il placeholder const rigaDati = sheet.getRange(currentRow, 1, 1, sheet.getLastColumn()).getValues()[0]; const dataPrenotazioneRaw = rigaDati[COL_DATA_PRENOTAZIONE]; const dataPrenotazione = dataPrenotazioneRaw instanceof Date ? Utilities.formatDate(dataPrenotazioneRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenotazioneRaw); const payloadData = { nomeCliente: rigaDati[COL_NOME], bookingId: rigaDati[COL_ID], dataPrenotazione: dataPrenotazione, numClienti: rigaDati[COL_NUM_CLIENTI], pacchettoScelto: rigaDati[COL_PACCHETTO], accontoImportoFormatted: (parseFloat(rigaDati[COL_ACCONTO]) || 0).toFixed(2).replace('.', ','), }; const template6 = getWhatsAppTemplate(MESSAGGIO_ACCONTO_PAGATO); const messaggio6 = replacePlaceholders(template6, payloadData); // La funzione getNumeriCassiere è definita in AutomazioneStaff.gs ma accessibile qui const numeriCassiere = getNumeriCassiere(); numeriCassiere.forEach(numero => { sendWhatsAppMessage(numero, messaggio6); }); Logger.log(`Notifica pagamento acconto inviata al Cassiere per ID ${bookingId}`); } catch (e) { Logger.log(`ERRORE nell'invio Messaggio6 al Cassiere per ID ${bookingId}: ${e.message}`); } break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); } // ========================================================= // --- 4. FUNZIONI WHATSAPP (CLIENTE & UTILITY) --- // ========================================================= /** * Legge il template del messaggio dal foglio 'Messaggi'. */ function getWhatsAppTemplate(key) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_MESSAGGI); if (!sheet) { Logger.log(`ERRORE: Foglio '${FOGLIO_MESSAGGI}' non trovato.`); return `ERRORE: Template non trovato per ${key}.`; } const values = sheet.getDataRange().getValues(); for (let i = 0; i < values.length; i++) { if (String(values[i][MESSAGGI_KEY_COL]).trim() === key) { return String(values[i][MESSAGGI_CONTENT_COL]); } } Logger.log(`ERRORE: Chiave template '${key}' non trovata nel foglio 'Messaggi'.`); return `ERRORE: Template non trovato per ${key}.`; } /** * Sostituisce i placeholder {...} nel template con i dati della prenotazione. */ function replacePlaceholders(template, data) { let message = template; // Mappatura dei placeholder con le chiavi in minuscolo e parentesi singole const placeholderMap = { '{nome_cliente}': data.nomeCliente, '{id_prenotazione}': data.bookingId, '{data_evento}': data.dataPrenotazione, '{numero_persone}': data.numClienti, '{pacchetto_scelto}': data.nomePacchetto, '{acconto_importo}': data.accontoImportoFormatted, '{saldo_importo}': data.saldoFormatted, '{link_stripe}': data.linkStripe, '{descrizione_menu}': data.descrizioneMenu || '', '{descrizione_animazione}': data.descrizioneAnimazione || '', '{note}': data.note || '' }; for (const placeholder in placeholderMap) { const value = placeholderMap[placeholder]; // Sostituisce la chiave, assicurandosi di fare l'escape delle parentesi graffe per RegExp message = message.replace(new RegExp(placeholder.replace(/([{}])/g, "\\$1"), 'g'), value); } // Gestione specifica dell'animazione per placeholder vecchio let animazioneText = ''; if (data.animazioneScelta && String(data.animazioneScelta).length > 0 && parseFloat(data.animazioneScelta) !== 0) { animazioneText = `Animazione scelta: ${data.animazioneScelta}`; } message = message.replace(/{animazione_scelta}/g, animazioneText); return message; } /** * Funzione di utilità per inviare il messaggio tramite API SendApp. */ function sendWhatsAppMessage(recipient, message) { if (!SENDAPP_API_URL || !SENDAPP_TOKEN || !SENDAPP_INSTANCE_ID) { Logger.log('Errore: Credenziali API SendApp non configurate correttamente.'); return false; } const apiUrl = SENDAPP_API_URL; // Pulizia e formattazione numero let telefono = String(recipient).replace(/[^\d\+]/g, ''); // Rimuove prefissi internazionali noti if (telefono.startsWith('+')) { telefono = telefono.substring(1); } else if (telefono.startsWith('00')) { telefono = telefono.substring(2); } // LOGICA DI CONTROLLO DEL PREFISSO ITALIANO if (telefono.startsWith('39')) { if (telefono.length > 12 && telefono.startsWith('3939')) { telefono = telefono.substring(2); } } else { telefono = '39' + telefono; } // CONTROLLO DI VALIDITÀ FINALE: un numero WhatsApp italiano valido è di 12 cifre (39 + 10 cifre). if (telefono.length < 11) { Logger.log(`AVVISO: Numero di telefono scartato (troppo corto/invalido): ${recipient}. Risultato pulito: ${telefono}`); return false; } const payload = { "access_token": SENDAPP_TOKEN, "instance_id": SENDAPP_INSTANCE_ID, "number": telefono, "message": message }; const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; try { const response = UrlFetchApp.fetch(apiUrl, options); const result = JSON.parse(response.getContentText()); if (result.status && result.status === 'success') { return true; } else { Logger.log(`Errore nell\'invio WhatsApp a ${telefono}: ${result.message || 'Errore sconosciuto API.'}`); return false; } } catch (e) { Logger.log('Errore di connessione API WhatsApp: ' + e.toString()); return false; } } /** * Funzione eseguita dal trigger a tempo per inviare i messaggi al cliente. */ function processPendingMessages() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); const lastRow = sheet.getLastRow(); if (lastRow <= 1) return; const maxCols = COL_DESCRIZIONE_ANIMAZIONE + 1; // Colonna W const range = sheet.getRange(2, 1, lastRow - 1, maxCols); const values = range.getValues(); for (let i = 0; i < values.length; i++) { const data = values[i]; const currentRow = i + 2; const statoAutomazione = String(data[COL_STATO_AUTOMAZIONE]).trim(); if (statoAutomazione !== STATO_LINK_GENERATO && statoAutomazione !== STATO_PRONTO_PER_INVIO) { continue; } // 1. Estrazione Dati const bookingId = data[COL_ID]; const telefono = data[COL_TELEFONO]; const nomeCliente = data[COL_NOME]; const dataPrenotazioneRaw = data[COL_DATA_PRENOTAZIONE]; const dataPrenotazione = dataPrenotazioneRaw instanceof Date ? Utilities.formatDate(dataPrenotazioneRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenotazioneRaw); const numClienti = data[COL_NUM_CLIENTI]; const nomePacchetto = data[COL_PACCHETTO]; const animazioneScelta = data[COL_ANIMAZIONE]; const importoAcconto = data[COL_ACCONTO]; const saldo = data[COL_SALDO]; const linkStripe = data[COL_LINK_STRIPE]; const note = data[COL_NOTE]; const descrizioneMenu = data[COL_DESCRIZIONE_MENU]; const descrizioneAnimazione = data[COL_DESCRIZIONE_ANIMAZIONE]; // 2. Preparazione dei dati per il placeholder const accontoValue = parseFloat(importoAcconto) || 0; const isAccontoRequired = (accontoValue > 0); const payloadData = { nomeCliente: nomeCliente, bookingId: bookingId, dataPrenotazione: dataPrenotazione, numClienti: numClienti, nomePacchetto: nomePacchetto, animazioneScelta: animazioneScelta, importoAccontoFormatted: accontoValue.toFixed(2).replace('.', ','), saldoFormatted: (parseFloat(saldo) || 0).toFixed(2).replace('.', ','), linkStripe: linkStripe, note: note, descrizioneMenu: descrizioneMenu, descrizioneAnimazione: descrizioneAnimazione }; // 3. INVIO e AGGIORNAMENTO STATO let messageKey = isAccontoRequired ? MESSAGGIO1_KEY : MESSAGGIO2_KEY; const rawTemplate = getWhatsAppTemplate(messageKey); const finalMessage = replacePlaceholders(rawTemplate, payloadData); const isSent = sendWhatsAppMessage(telefono, finalMessage); const statoColIndex = COL_STATO_AUTOMAZIONE + 1; const statoCell = sheet.getRange(currentRow, statoColIndex); if (isSent) { statoCell.setValue(STATO_INVIATO); } else { statoCell.setValue(STATO_ERRORE_INVIO + ` (${statoAutomazione})`); Logger.log(`Tentativo fallito per ID ${bookingId}.`); } } }

{nome_cliente}Il nome inserito dal cliente nel form.Mario Rossi

{id_prenotazione}Il codice identificativo univoco.DA260778

{data_evento}La data per cui hanno prenotato.15/12/2025

{numero_persone}Il numero di persone indicate.4

{pacchetto_scelto}Il nome del Menù o Pacchetto selezionato.Menù Pizza

{link_stripe}Il link cliccabile per il pagamento.https://buy.stripe.com/

{importo_acconto}La cifra esatta dell’acconto (se serve).25.00

// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI FOGLIO const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; // Rimosso riferimento a FOGLIO_MESSAGGI // VARIABILI ID / CONTATORI const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; const COL_DATA_PRENOTAZIONE = 2; const COL_NOME = 3; const COL_TELEFONO = 4; const COL_NUM_CLIENTI = 5; const COL_PACCHETTO = 6; const COL_ANIMAZIONE = 7; const COL_ACCONTO = 13; const COL_SALDO = 14; const COL_STATO = 15; const COL_LINK_STRIPE = 16; const COL_STATO_AUTOMAZIONE = 17; // Manteniamo l'indice per non rompere l'ordine colonne, ma non verrà usato. // Rimosse VARIABILI WHATSAPP (SENDAPP) e STATI DI AUTOMAZIONE // ========================================================= // --- 2. FUNZIONI PRINCIPALI (doGet, doPost) --- // ========================================================= function doGet(e) { const params = e.parameter; if (params.action === 'getMenus') { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages })).setMimeType(ContentService.MimeType.JSON); } catch (error) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore: ' + error.message })).setMimeType(ContentService.MimeType.JSON); } } return HtmlService.createHtmlOutput('Servizio prenotazioni attivo (No WhatsApp).'); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { return handleStripeWebhook(e); } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } // Se l'acconto è richiesto, si usa il placeholder per poi generare il link const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, '', // COL R: Stato Automazione lasciato VUOTO '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '' ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione (se richiesto).' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 3. FUNZIONI STRIPE & MENU --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione da eseguire con trigger per generare i link Stripe. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { // Controlla se è necessario generare il link if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); // Rimossa logica di aggiornamento stato WhatsApp } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE nella creazione del link'); } } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); }
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI FOGLIO const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; // VARIABILI ID / CONTATORI const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; const COL_DATA_PRENOTAZIONE = 2; const COL_NOME = 3; const COL_TELEFONO = 4; const COL_NUM_CLIENTI = 5; const COL_PACCHETTO = 6; const COL_ANIMAZIONE = 7; const COL_ACCONTO = 13; const COL_SALDO = 14; const COL_STATO = 15; const COL_LINK_STRIPE = 16; const COL_STATO_AUTOMAZIONE = 17; // ========================================================= // --- 2. GESTIONE WEB APP (DO GET AGGIORNATO) --- // ========================================================= function doGet(e) { const params = e.parameter; const action = params.action; // 1. VECCHIA LOGICA: Richiesta MENU (Per il form di prenotazione) if (action === 'getMenus') { // Richiama la logica dei menu e restituisce il JSON return createCorsResponse(handleGetMenus(params)); } // 2. NUOVA LOGICA: Richiesta CALENDARIO (Per la dashboard) if (action === 'getCalendarData') { // Recupera i dati dal foglio e restituisce il JSON return createCorsResponse(getDataForWeb()); } // Default: se apri il link nel browser senza parametri return HtmlService.createHtmlOutput('Servizio prenotazioni attivo (API v2).'); } // ========================================================= // --- FUNZIONI DI SUPPORTO PER IL WEB (DA AGGIUNGERE) --- // ========================================================= // Funzione che gestisce la logica dei menu (esattamente come avevi prima) function handleGetMenus(params) { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages }); } catch (error) { return JSON.stringify({ status: 'error', message: 'Errore: ' + error.message }); } } // Funzione che estrae i dati per il Calendario Web function getDataForWeb() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); const data = sheet.getDataRange().getValues(); const output = []; // Saltiamo l'intestazione (i=1) for (let i = 1; i < data.length; i++) { const row = data[i]; const rawDate = row[COL_DATA_PRENOTAZIONE]; // Colonna C // Controlliamo che ci sia una data valida if (rawDate instanceof Date) { output.push({ id: row[COL_ID], nome: row[COL_NOME], telefono: row[COL_TELEFONO], // Formattiamo la data per farla leggere al calendario (YYYY-MM-DD) data: Utilities.formatDate(rawDate, Session.getScriptTimeZone(), 'yyyy-MM-dd'), pax: row[COL_NUM_CLIENTI], pacchetto: row[COL_PACCHETTO], note: row[9], // Colonna J acconto: row[COL_ACCONTO], saldo: row[COL_SALDO], statoStripe: row[COL_STATO], menuDesc: row[21], // V animazione: row[22] // W }); } } return JSON.stringify(output); } // Funzione fondamentale per permettere al tuo sito esterno di leggere i dati function createCorsResponse(dataString) { return ContentService.createTextOutput(dataString) .setMimeType(ContentService.MimeType.JSON); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { return handleStripeWebhook(e); } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // 📢 NUOVA LOGICA: APPLICAZIONE CAPITALIZZAZIONE (MAIUSC.INIZ) // Applicata a 'Nome' (Colonna D) e altri campi di testo // ==================================================== if (data.nome) { data.nome = formattaNomiAutomatico(data.nome); } if (data.pacchetto) { data.pacchetto = formattaNomiAutomatico(data.pacchetto); } if (data.note) { data.note = formattaNomiAutomatico(data.note); } // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } // Se l'acconto è richiesto, si usa il placeholder per poi generare il link const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, // data.nome è ora formattato! parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, '', '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '' ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione (se richiesto).' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 3. FUNZIONI STRIPE & MENU --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione da eseguire con trigger per generare i link Stripe. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { // Controlla se è necessario generare il link if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE nella creazione del link'); } } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); } // ========================================================= // --- 4. FUNZIONI DI FORMATTAZIONE (NUOVA SEZIONE) --- // ========================================================= /** * Converte una stringa in formato MAIUSC.INIZ (Proper Case), * capitalizzando la prima lettera di ogni parola e rendendo le altre minuscole. * Esempio: "paolo rossi" -> "Paolo Rossi" * @param {string} stringa La stringa da formattare. * @returns {string} La stringa formattata. */ function formattaNomiAutomatico(stringa) { if (typeof stringa !== 'string' || stringa.length === 0) { return stringa; } // Rimuovi spazi extra e converti in minuscolo var stringaPulita = stringa.trim().toLowerCase(); // Applica la maiuscola alla prima lettera di ogni parola var properCased = stringaPulita.split(' ').map(function(word) { if (word.length > 0) { return word.charAt(0).toUpperCase() + word.slice(1); } return ''; }).join(' '); return properCased; }
Prenota
20/12/2025

Riepilogo Costi

Totale Cena (Gruppo): 0,00 €
Importo Totale 0,00 €
Costo per persona: 0,00 €
Acconto da versare (25%): 0,00 €
Saldo da pagare al Disco Penelope: 0,00 €
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI FOGLIO const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; // VARIABILI ID / CONTATORI const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_dbPnYHXPIsrHhH4ISDht9QqpMACQfvtB'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; const COL_DATA_PRENOTAZIONE = 2; const COL_NOME = 3; const COL_TELEFONO = 4; const COL_NUM_CLIENTI = 5; const COL_PACCHETTO = 6; const COL_ANIMAZIONE = 7; const COL_ACCONTO = 13; const COL_SALDO = 14; const COL_STATO = 15; const COL_LINK_STRIPE = 16; const COL_STATO_AUTOMAZIONE = 17; // ========================================================= // --- 2. GESTIONE WEB APP (DO GET AGGIORNATO) --- // ========================================================= function doGet(e) { const params = e.parameter; const action = params.action; // 1. VECCHIA LOGICA: Richiesta MENU (Per il form di prenotazione) if (action === 'getMenus') { // Richiama la logica dei menu e restituisce il JSON return createCorsResponse(handleGetMenus(params)); } // 2. NUOVA LOGICA: Richiesta CALENDARIO (Per la dashboard) if (action === 'getCalendarData') { // Recupera i dati dal foglio e restituisce il JSON return createCorsResponse(getDataForWeb()); } // Default: se apri il link nel browser senza parametri return HtmlService.createHtmlOutput('Servizio prenotazioni attivo (API v2).'); } // ========================================================= // --- FUNZIONI DI SUPPORTO PER IL WEB (DA AGGIUNGERE) --- // ========================================================= // Funzione che gestisce la logica dei menu (esattamente come avevi prima) function handleGetMenus(params) { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages }); } catch (error) { return JSON.stringify({ status: 'error', message: 'Errore: ' + error.message }); } } // Funzione che estrae i dati per il Calendario Web function getDataForWeb() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); const data = sheet.getDataRange().getValues(); const output = []; // Saltiamo l'intestazione (i=1) for (let i = 1; i < data.length; i++) { const row = data[i]; const rawDate = row[COL_DATA_PRENOTAZIONE]; // Colonna C // Controlliamo che ci sia una data valida if (rawDate instanceof Date) { output.push({ id: row[COL_ID], nome: row[COL_NOME], telefono: row[COL_TELEFONO], // Formattiamo la data per farla leggere al calendario (YYYY-MM-DD) data: Utilities.formatDate(rawDate, Session.getScriptTimeZone(), 'yyyy-MM-dd'), pax: row[COL_NUM_CLIENTI], pacchetto: row[COL_PACCHETTO], note: row[9], // Colonna J acconto: row[COL_ACCONTO], saldo: row[COL_SALDO], statoStripe: row[COL_STATO], menuDesc: row[21], // V animazione: row[22] // W }); } } return JSON.stringify(output); } // Funzione fondamentale per permettere al tuo sito esterno di leggere i dati function createCorsResponse(dataString) { return ContentService.createTextOutput(dataString) .setMimeType(ContentService.MimeType.JSON); } function doPost(e) { let payload; try { payload = JSON.parse(e.postData.contents); } catch (error) { if (e.postData.contents && e.parameter.event_type) { return handleStripeWebhook(e); } return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Dati non validi.' })) .setMimeType(ContentService.MimeType.JSON); } // WEBHOOK STRIPE if (payload.type && (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded')) { const stripeEvent = payload; let clientReferenceId = null; if (stripeEvent.data.object.metadata && stripeEvent.data.object.metadata.booking_id) { clientReferenceId = stripeEvent.data.object.metadata.booking_id; } if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('Ignored').setMimeType(ContentService.MimeType.TEXT); } // GESTIONE INVIO MODULO const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); const data = payload; // ==================================================== // 📢 NUOVA LOGICA: APPLICAZIONE CAPITALIZZAZIONE (MAIUSC.INIZ) // Applicata a 'Nome' (Colonna D) e altri campi di testo // ==================================================== if (data.nome) { data.nome = formattaNomiAutomatico(data.nome); } if (data.pacchetto) { data.pacchetto = formattaNomiAutomatico(data.pacchetto); } if (data.note) { data.note = formattaNomiAutomatico(data.note); } // ==================================================== // CALCOLO ID DINAMICO E SEQUENZIALE // ==================================================== const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) { maxId = id; } } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; const dateParts = data.data.split('-'); const dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } // Se l'acconto è richiesto, si usa il placeholder per poi generare il link const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, // data.nome è ora formattato! parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, '', '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '' ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata. Link Stripe in generazione (se richiesto).' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 3. FUNZIONI STRIPE & MENU --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } /** * Funzione da eseguire con trigger per generare i link Stripe. */ function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { // Controlla se è necessario generare il link if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE nella creazione del link'); } } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][COL_ID] == bookingId) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); break; } } } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); } // ========================================================= // --- 4. FUNZIONI DI FORMATTAZIONE (NUOVA SEZIONE) --- // ========================================================= /** * Converte una stringa in formato MAIUSC.INIZ (Proper Case), * capitalizzando la prima lettera di ogni parola e rendendo le altre minuscole. * Esempio: "paolo rossi" -> "Paolo Rossi" * @param {string} stringa La stringa da formattare. * @returns {string} La stringa formattata. */ function formattaNomiAutomatico(stringa) { if (typeof stringa !== 'string' || stringa.length === 0) { return stringa; } // Rimuovi spazi extra e converti in minuscolo var stringaPulita = stringa.trim().toLowerCase(); // Applica la maiuscola alla prima lettera di ogni parola var properCased = stringaPulita.split(' ').map(function(word) { if (word.length > 0) { return word.charAt(0).toUpperCase() + word.slice(1); } return ''; }).join(' '); return properCased; }
// =========================================== // --- 1. CONFIGURAZIONE GLOBALE & STRIPE --- // =========================================== // VARIABILI GLOBALI FOGLIO const FOGLIO_PRINCIPALE = 'Prenotazioni'; const FOGLIO_CALENDARIO = 'Calendario'; const FOGLIO_ANIMAZIONE = 'Animazione'; // VARIABILI ID / CONTATORI const ID_INIZIALE_NUMERICO = 260777; const ID_PREFIX = 'DA'; const ID_NUMERICO_COL = 19; // Colonna T (Base 0 = 19) // VARIABILI STRIPE const STRIPE_SECRET_KEY = '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'; const STRIPE_SUCCESS_URL = 'https://discopenelope.it/thank-prenotazione/'; const CURRENCY = 'eur'; const STRIPE_WEBHOOK_SECRET = 'whsec_zHUGfK30AkTzCLVYcckIeoACayHVwVyc'; // --- INDICI FOGLIO (Base 0) --- const COL_ID = 0; const COL_DATA_PRENOTAZIONE = 2; const COL_NOME = 3; const COL_TELEFONO = 4; const COL_NUM_CLIENTI = 5; const COL_PACCHETTO = 6; const COL_ANIMAZIONE = 7; const COL_ACCONTO = 13; const COL_SALDO = 14; const COL_STATO = 15; const COL_LINK_STRIPE = 16; const COL_STATO_AUTOMAZIONE = 17; // ========================================================= // --- 2. GESTIONE WEB APP (DO GET) --- // ========================================================= function doGet(e) { const params = e.parameter; const action = params.action; // 1. Richiesta MENU (Per il form di prenotazione) if (action === 'getMenus') { return createCorsResponse(handleGetMenus(params)); } // 2. Richiesta CALENDARIO (Per la dashboard) if (action === 'getCalendarData') { return createCorsResponse(getDataForWeb()); } // Default: Messaggio di stato return HtmlService.createHtmlOutput('Servizio prenotazioni attivo (API v2 - Updated).'); } // ========================================================= // --- 3. GESTIONE WEB APP (DO POST - CORRECTED) --- // ========================================================= function doPost(e) { // Tentativo di leggere il payload JSON (per Stripe o dati sito inviati come JSON) let payload = null; let isJson = false; try { if (e.postData && e.postData.contents) { payload = JSON.parse(e.postData.contents); isJson = true; } } catch (error) { // Se fallisce il parse, probabilmente non è un JSON valido o è un form-data standard isJson = false; } // --- A. GESTIONE WEBHOOK STRIPE --- // Se è un JSON e contiene 'type', assumiamo sia Stripe if (isJson && payload && payload.type) { try { // Filtriamo solo gli eventi che ci interessano if (payload.type === 'checkout.session.completed' || payload.type === 'payment_intent.succeeded') { const stripeEvent = payload; let clientReferenceId = null; // Cerchiamo l'ID prenotazione nei metadata if (stripeEvent.data && stripeEvent.data.object) { const obj = stripeEvent.data.object; if (obj.metadata && obj.metadata.booking_id) { clientReferenceId = obj.metadata.booking_id; } else if (obj.client_reference_id) { clientReferenceId = obj.client_reference_id; } } // Se abbiamo trovato l'ID, aggiorniamo il foglio if (clientReferenceId) { updateRowStatus(clientReferenceId); return ContentService.createTextOutput('ACK: Updated').setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput('ACK: Ignored (No ID)').setMimeType(ContentService.MimeType.TEXT); } // Rispondiamo OK a tutti gli altri eventi Stripe per non generare errori return ContentService.createTextOutput('ACK: Event handled').setMimeType(ContentService.MimeType.TEXT); } catch (stripeError) { // In caso di errore nel codice, logghiamo ma rispondiamo 200 a Stripe per evitare retry infiniti console.error("Errore Webhook: " + stripeError.toString()); return ContentService.createTextOutput('Error Handled').setMimeType(ContentService.MimeType.TEXT); } } // --- B. GESTIONE NUOVA PRENOTAZIONE (DAL SITO) --- // Se siamo arrivati qui, NON è un webhook di Stripe, quindi è una prenotazione try { // Se non abbiamo parsato il payload prima, usiamo e.parameter (per form standard) o riproviamo il parse const data = (isJson) ? payload : e.parameter; return gestisciNuovaPrenotazione(data); } catch (bookingError) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Errore salvataggio: ' + bookingError.toString() })).setMimeType(ContentService.MimeType.JSON); } } // Funzione separata per salvare la prenotazione (più pulita) function gestisciNuovaPrenotazione(data) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(FOGLIO_PRINCIPALE); // Capitalizzazione nomi if (data.nome) data.nome = formattaNomiAutomatico(data.nome); if (data.pacchetto) data.pacchetto = formattaNomiAutomatico(data.pacchetto); if (data.note) data.note = formattaNomiAutomatico(data.note); // Calcolo ID Dinamico const ultimaRiga = sheet.getLastRow(); let nuovoID_Numerico; if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID' || ultimaRiga === 1) { if (ultimaRiga === 0 || sheet.getRange('A1').getValue() !== 'ID') { sheet.appendRow(['ID', 'Data Creazione', 'Data Prenotazione', 'Nome', 'Telefono', 'N° Clienti', 'Menu', 'Animazione (Costo)', 'Extra', 'Note', 'Richiedi Acconto', 'Totale', 'Sconto', 'Importo Acconto', 'Tot. Da Pagare', 'Stato Acconto', 'Link Stripe', 'Stato Automazione', 'Email Cliente', 'ID_Numerico', 'Tipo Prenotazione', 'Descrizione Menu', 'Descrizione Animazione']); } nuovoID_Numerico = ID_INIZIALE_NUMERICO; } else { const ultimoIDValore = sheet.getRange(ultimaRiga, ID_NUMERICO_COL + 1).getValue(); let ultimoID = parseInt(ultimoIDValore, 10) || 0; if (ultimoID < ID_INIZIALE_NUMERICO) { const idValues = sheet.getRange(2, ID_NUMERICO_COL + 1, ultimaRiga - 1, 1).getValues(); let maxId = ID_INIZIALE_NUMERICO - 1; for (let i = 0; i < idValues.length; i++) { const id = parseInt(idValues[i][0], 10); if (!isNaN(id) && id > maxId) maxId = id; } nuovoID_Numerico = (maxId >= ID_INIZIALE_NUMERICO) ? maxId + 1 : ID_INIZIALE_NUMERICO; } else { nuovoID_Numerico = ultimoID + 1; } } const sourcePrefix = (data.source && data.source.trim().toUpperCase()) || ID_PREFIX; const nuovoID = sourcePrefix + nuovoID_Numerico; // Preparazione Dati const dataCorrente = new Date(); const totale = parseFloat(data.totale) || 0; const importoAcconto = parseFloat(data.acconto) || 0; const costoAnimazione = parseFloat(data.extra) || 0; const totDaPagare = totale - importoAcconto; let dataCliente; if (data.data && data.data.includes('-')) { const dateParts = data.data.split('-'); dataCliente = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); } else { dataCliente = new Date(); // Fallback } let tipoPrenotazione; switch (sourcePrefix) { case 'DA': tipoPrenotazione = 'Cena'; break; case 'PC': tipoPrenotazione = 'Pre-Cena'; break; case 'DOPOCENA': tipoPrenotazione = 'Dopocena'; break; default: tipoPrenotazione = sourcePrefix; } const stripeLinkPlaceholder = (importoAcconto > 0) ? 'LINK_STRIPE_TEMP' : 'Nessun acconto richiesto'; const riga = [ nuovoID, dataCorrente, dataCliente, data.nome, data.telefono, parseInt(data.numClienti, 10), data.pacchetto, costoAnimazione, 0, data.note, data.richiediAcconto, totale, 0, importoAcconto, totDaPagare, 'Acconto da pagare', stripeLinkPlaceholder, '', '', nuovoID_Numerico, tipoPrenotazione, data.descrizioneMenu || '', data.descrizioneAnimazione || '' ]; sheet.appendRow(riga); return ContentService.createTextOutput(JSON.stringify({ status: 'success', id: nuovoID, message: 'Prenotazione salvata.' })).setMimeType(ContentService.MimeType.JSON); } // ========================================================= // --- 4. FUNZIONI STRIPE, MENU & UTILITIES --- // ========================================================= function convertObjectToUrlEncodedString(obj) { return Object.keys(obj).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).join('&'); } function createStripePaymentLink(amount, bookingId, packageName) { const numericAmount = parseFloat(amount); if (!numericAmount || numericAmount <= 0) return 'Nessun acconto.'; // FIX CRITICO: Math.round assicura che inviamo un intero (es. 1750 non 1750.0) const amountInCents = Math.round(numericAmount * 100); const productName = `Acconto Prenotazione ${bookingId}`; const priceOptions = { 'method': 'post', 'headers': { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, 'payload': convertObjectToUrlEncodedString({ 'unit_amount': amountInCents, // Ora siamo sicuri che è un intero 'currency': CURRENCY, 'product_data[name]': productName, }), 'muteHttpExceptions': true }; try { const pRes = UrlFetchApp.fetch('https://api.stripe.com/v1/prices', priceOptions); const pJson = JSON.parse(pRes.getContentText()); // Se Stripe restituisce errore sul prezzo, lo vediamo qui if (!pJson.id) return 'Err Price: ' + JSON.stringify(pJson); const lRes = UrlFetchApp.fetch('https://api.stripe.com/v1/payment_links', { method: 'post', headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' }, payload: convertObjectToUrlEncodedString({ 'line_items[0][price]': pJson.id, 'line_items[0][quantity]': 1, 'after_completion[type]': 'redirect', 'after_completion[redirect][url]': STRIPE_SUCCESS_URL + '?client_reference_id=' + bookingId, 'metadata[booking_id]': bookingId }), muteHttpExceptions: true }); const lJson = JSON.parse(lRes.getContentText()); return lJson.url || ('Err Link: ' + JSON.stringify(lJson)); } catch (e) { return 'Err Net: ' + e.toString(); } } function processStripeLinks() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const values = sheet.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { if (values[i][COL_LINK_STRIPE] === 'LINK_STRIPE_TEMP' && values[i][COL_ACCONTO] > 0) { const bookingId = values[i][COL_ID]; const accontoAmount = values[i][COL_ACCONTO]; const packageName = values[i][COL_PACCHETTO]; const newLink = createStripePaymentLink(accontoAmount, bookingId, packageName); sheet.getRange(i + 1, COL_LINK_STRIPE + 1).setValue(newLink); if (newLink.startsWith('http')) { sheet.getRange(i + 1, COL_STATO + 1).setValue('Link Stripe Generato'); } else { sheet.getRange(i + 1, COL_STATO + 1).setValue('ERRORE STRIPE'); } } } } function updateRowStatus(bookingId) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); if (!sheet) return; const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (String(data[i][COL_ID]) === String(bookingId)) { sheet.getRange(i + 1, COL_STATO + 1).setValue('ACCONTO PAGATO'); SpreadsheetApp.flush(); // Forza il salvataggio immediato break; } } } function handleGetMenus(params) { try { const dateString = params.date; const tipo = params.type; const dateParts = dateString.split('-'); const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; const menus = getMenuPackages(formattedDate, tipo); const animationPackages = getAnimationPackages(); return JSON.stringify({ status: 'success', options: menus, animationOptions: animationPackages }); } catch (error) { return JSON.stringify({ status: 'error', message: 'Errore: ' + error.message }); } } function getDataForWeb() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_PRINCIPALE); const data = sheet.getDataRange().getValues(); const output = []; for (let i = 1; i < data.length; i++) { const row = data[i]; const rawDate = row[COL_DATA_PRENOTAZIONE]; if (rawDate instanceof Date) { output.push({ id: row[COL_ID], nome: row[COL_NOME], telefono: row[COL_TELEFONO], data: Utilities.formatDate(rawDate, Session.getScriptTimeZone(), 'yyyy-MM-dd'), pax: row[COL_NUM_CLIENTI], pacchetto: row[COL_PACCHETTO], note: row[9], acconto: row[COL_ACCONTO], saldo: row[COL_SALDO], statoStripe: row[COL_STATO], menuDesc: row[21], animazione: row[22] }); } } return JSON.stringify(output); } function createCorsResponse(dataString) { return ContentService.createTextOutput(dataString).setMimeType(ContentService.MimeType.JSON); } function getMenuPackages(dateString, tipo) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_CALENDARIO); const data = sheet.getDataRange().getValues().slice(1); const parts = dateString.split('/'); const filtered = data.filter(row => { const d = row[1]; const rowDate = (Object.prototype.toString.call(d) === '[object Date]') ? `${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()}` : String(d); const inDate = dateString.replace(/^0+/g, '').replace(/\/0+/g, '/'); return String(row[0]).toLowerCase() === tipo.toLowerCase() && rowDate === inDate; }); if (filtered.length > 0) return filtered.map(formatMenuRow); const dateObj = new Date(parts[2], parts[1]-1, parts[0]); const day = dateObj.getDay(); let typeDay = (day === 1) ? 'Lunedì' : ((day===0||day>4) ? 'Weekend' : 'Feriale'); if(day===5 || day===6) typeDay = 'Weekend'; if (typeDay === 'Lunedì') { const closed = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])==='Lunedì' && String(r[2]).includes('chiusi')); if(closed.length>0) return [{value: `CHIUSO: ${closed[0][2]}`, description: closed[0][3], price: 0, sexyCamAllowed: false}]; } const res = data.filter(r => String(r[0]).toLowerCase()===tipo.toLowerCase() && String(r[1])===typeDay).map(formatMenuRow); return res.length ? res : [{value: 'Nessun Menù', description: '', price: 0}]; } function formatMenuRow(row) { return { value: `${row[2]} (€ ${Number(row[4]).toFixed(2)})`, description: row[3], price: row[4], sexyCamAllowed: String(row[5]).toLowerCase() === 'si' }; } function getAnimationPackages() { const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOGLIO_ANIMAZIONE).getDataRange().getValues().slice(1); return data.map(r => ({ value: `${r[0]} (€ ${Number(r[1]).toFixed(2)})`, price: r[1], description: r[2] })).filter(i => i.price > 0); } function formattaNomiAutomatico(stringa) { if (typeof stringa !== 'string' || stringa.length === 0) return stringa; var stringaPulita = stringa.trim().toLowerCase(); return stringaPulita.split(' ').map(function(word) { if (word.length > 0) return word.charAt(0).toUpperCase() + word.slice(1); return ''; }).join(' '); }
// ========================================================= // --- FILE: WhatsApp.gs (COMPLETO: Clienti, Staff, Cassieri, Solleciti, Riepilogo) --- // ========================================================= // --- 1. CONFIGURAZIONE SENDAPP --- const WA_API_URL = 'https://app.sendapp.cloud/api/send'; const WA_TOKEN = '68863b87201fc'; const WA_INSTANCE = '68863C08859E7'; // --- 2. CONFIGURAZIONE FOGLI E RUOLI --- const FOGLIO_STAFF = 'Staff'; const RUOLO_DIRETTORE = 'Direttore'; const RUOLO_PERSONALE = 'Personale'; const RUOLO_CASSIERE = 'Cassiere'; // <--- NUOVO RUOLO // Chiavi nel foglio 'Messaggi' const KEY_MSG_ACCONTO = 'Messaggio1'; const KEY_MSG_CONFERMA = 'Messaggio2'; const KEY_MSG_STAFF = 'Messaggio5'; // Direttore (Nuova Prenotazione) const KEY_MSG_CASSIERE = 'Messaggio6'; // Cassiere (Pagamento Ricevuto) <--- NUOVO const KEY_MSG_SOLLECITO = 'Messaggio7'; // --- 3. CONFIGURAZIONE COLONNE (Indici Base 0: A=0) --- const INDEX_COL_STATO_STAFF = 23; // Colonna X: Stato invio Direttore const TESTO_CONFERMA_STAFF = 'Messaggio5 inviato'; const INDEX_COL_SOLLECITO = 25; // Colonna Z: Stato invio Sollecito const TESTO_CONFERMA_SOLLECITO = 'Sollecito Inviato'; const INDEX_COL_CASSIERE = 26; // Colonna AA: Stato invio Cassiere <--- NUOVO const TESTO_CONFERMA_CASSIERE = 'Messaggio6 inviato'; // ========================================================= // --- 1. AUTOMAZIONE MINUTO PER MINUTO --- // ========================================================= function processWhatsAppMessages() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetPrenotazioni = ss.getSheetByName(FOGLIO_PRINCIPALE); const sheetMessaggi = ss.getSheetByName('Messaggi'); const sheetStaff = ss.getSheetByName(FOGLIO_STAFF); if (!sheetPrenotazioni || !sheetMessaggi || !sheetStaff) return; // Caricamento Template const templateAcconto = getTemplateFromSheet(sheetMessaggi, KEY_MSG_ACCONTO); const templateConferma = getTemplateFromSheet(sheetMessaggi, KEY_MSG_CONFERMA); const templateStaff = getTemplateFromSheet(sheetMessaggi, KEY_MSG_STAFF); const templateCassiere = getTemplateFromSheet(sheetMessaggi, KEY_MSG_CASSIERE); // <--- NUOVO // Caricamento Numeri Staff const numeriDirettori = getNumeriStaffByRuolo(sheetStaff, RUOLO_DIRETTORE); const numeriCassieri = getNumeriStaffByRuolo(sheetStaff, RUOLO_CASSIERE); // <--- NUOVO if (!templateAcconto || !templateConferma || !templateStaff || !templateCassiere) return; const lastRow = sheetPrenotazioni.getLastRow(); if (lastRow <= 1) return; const values = sheetPrenotazioni.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { const row = values[i]; const rowIndex = i + 1; // A. GESTIONE CLIENTE (Colonna R) gestioneInvioCliente(sheetPrenotazioni, row, rowIndex, templateAcconto, templateConferma); // B. GESTIONE DIRETTORE (Colonna X - Nuova Prenotazione) const statoStaff = row[INDEX_COL_STATO_STAFF] ? String(row[INDEX_COL_STATO_STAFF]) : ''; if (statoStaff !== TESTO_CONFERMA_STAFF && numeriDirettori.length > 0) { // Usa la funzione generica passando colonna e testo specifici inviaNotificaStaff(sheetPrenotazioni, rowIndex, row, templateStaff, numeriDirettori, INDEX_COL_STATO_STAFF, TESTO_CONFERMA_STAFF); } // C. GESTIONE CASSIERE (Colonna AA - Acconto Pagato) const statoPagamento = String(row[COL_STATO]).toUpperCase(); // Colonna P const statoCassiere = row[INDEX_COL_CASSIERE] ? String(row[INDEX_COL_CASSIERE]) : ''; // Se è PAGATO e non abbiamo ancora avvisato il cassiere if (statoPagamento.includes('PAGATO') && statoCassiere !== TESTO_CONFERMA_CASSIERE && numeriCassieri.length > 0) { inviaNotificaStaff(sheetPrenotazioni, rowIndex, row, templateCassiere, numeriCassieri, INDEX_COL_CASSIERE, TESTO_CONFERMA_CASSIERE); } } } function gestioneInvioCliente(sheet, row, rowIndex, tplAcconto, tplConferma) { const statoStripe = String(row[COL_STATO]); const linkStripe = String(row[COL_LINK_STRIPE]); const statoAuto = String(row[COL_STATO_AUTOMAZIONE]); if (statoAuto.includes('Inviato') || statoAuto.includes('Errore')) return; if (statoStripe.includes('Link Stripe Generato') && linkStripe.startsWith('http')) { inviaMessaggioWA(sheet, rowIndex, COL_STATO_AUTOMAZIONE, row, tplAcconto, 'Inviato', false); } else if (linkStripe === 'Nessun acconto richiesto') { inviaMessaggioWA(sheet, rowIndex, COL_STATO_AUTOMAZIONE, row, tplConferma, 'Inviato', false); } } // Funzione generica aggiornata per gestire sia Direttori che Cassieri function inviaNotificaStaff(sheet, rowIndex, row, template, numeriDestinatari, colIndexTarget, testoConferma) { let success = 0; numeriDestinatari.forEach(numero => { // Passiamo colIndexTarget e testoConferma alla funzione di invio if (inviaMessaggioWA(sheet, rowIndex, colIndexTarget, row, template, testoConferma, true, numero)) success++; }); // Se almeno uno è partito, aggiorniamo la colonna specifica if (success > 0) sheet.getRange(rowIndex, colIndexTarget + 1).setValue(testoConferma); } // ========================================================= // --- 2. AUTOMAZIONE SOLLECITO (Giorno prima h 18:00) --- // ========================================================= function inviaSollecitoAcconto() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetPrenotazioni = ss.getSheetByName(FOGLIO_PRINCIPALE); const sheetMessaggi = ss.getSheetByName('Messaggi'); if (!sheetPrenotazioni || !sheetMessaggi) return; const templateSollecito = getTemplateFromSheet(sheetMessaggi, KEY_MSG_SOLLECITO); if (!templateSollecito) return; const oggi = new Date(); const domani = new Date(oggi); domani.setDate(oggi.getDate() + 1); const dataDomaniString = Utilities.formatDate(domani, Session.getScriptTimeZone(), 'dd/MM/yyyy'); const values = sheetPrenotazioni.getDataRange().getValues(); for (let i = 1; i < values.length; i++) { const row = values[i]; const rowIndex = i + 1; const dataPrenRaw = row[COL_DATA_PRENOTAZIONE]; let dataPrenString = (dataPrenRaw instanceof Date) ? Utilities.formatDate(dataPrenRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenRaw); const acconto = parseFloat(row[COL_ACCONTO] || 0); const statoPagamento = String(row[COL_STATO]).toUpperCase(); const statoSollecito = row[INDEX_COL_SOLLECITO] ? String(row[INDEX_COL_SOLLECITO]) : ''; if (dataPrenString === dataDomaniString && acconto > 0 && !statoPagamento.includes('PAGATO') && statoSollecito !== TESTO_CONFERMA_SOLLECITO) { inviaMessaggioWA(sheetPrenotazioni, rowIndex, INDEX_COL_SOLLECITO, row, templateSollecito, TESTO_CONFERMA_SOLLECITO, false); } } } // ========================================================= // --- 3. RIEPILOGO SERALE (Tutte le sere h 23:00) --- // ========================================================= function inviaRiepilogoSerale() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheetPrenotazioni = ss.getSheetByName(FOGLIO_PRINCIPALE); const sheetStaff = ss.getSheetByName(FOGLIO_STAFF); if (!sheetPrenotazioni || !sheetStaff) return; const oggi = new Date(); const domani = new Date(oggi); domani.setDate(oggi.getDate() + 1); const dataDomaniString = Utilities.formatDate(domani, Session.getScriptTimeZone(), 'dd/MM/yyyy'); const numeriPersonale = getNumeriStaffByRuolo(sheetStaff, RUOLO_PERSONALE); if (numeriPersonale.length === 0) return; const data = sheetPrenotazioni.getDataRange().getValues(); let elencoPrenotazioni = ""; let contatorePax = 0; let contatoreTavoli = 0; for (let i = 1; i < data.length; i++) { const riga = data[i]; const dataPrenRaw = riga[COL_DATA_PRENOTAZIONE]; let dataPrenString = (dataPrenRaw instanceof Date) ? Utilities.formatDate(dataPrenRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenRaw); if (dataPrenString === dataDomaniString) { const nome = riga[COL_NOME]; const pax = riga[COL_NUM_CLIENTI]; const telefono = riga[COL_TELEFONO]; const descMenu = riga[21]; const tipoAnimazione = riga[22]; const totale = parseFloat(riga[11] || 0).toFixed(2); const sconto = parseFloat(riga[12] || 0).toFixed(2); const acconto = parseFloat(riga[COL_ACCONTO] || 0).toFixed(2); const daPagare = parseFloat(riga[COL_SALDO] || 0).toFixed(2); const rawStato = String(riga[15]).toUpperCase(); let etichettaStato = rawStato.includes('PAGATO') ? "🟢 *ACCONTO PAGATO*" : (parseFloat(acconto) > 0 ? "🔴 *DA PAGARE*" : "⚪ Nessun Acconto"); elencoPrenotazioni += `👤 *${nome}* (x${pax})\n📞 Tel: ${telefono}\n🍽 Menù: ${descMenu}\n`; if (tipoAnimazione) elencoPrenotazioni += `🎭 Animazione: ${tipoAnimazione}\n`; elencoPrenotazioni += `💰 Tot: €${totale} | Sconto: €${sconto}\n💳 Acconto: €${acconto} → ${etichettaStato}\n💵 *SALDO CASSA: €${daPagare}*\n------------------------------\n`; contatorePax += Number(pax); contatoreTavoli++; } } let messaggioFinale = (contatoreTavoli === 0) ? `📅 *Riepilogo per Domani (${dataDomaniString})*\n\nNessuna prenotazione prevista. 😴` : `📅 *Riepilogo per Domani (${dataDomaniString})*\n\n📊 Tavoli: ${contatoreTavoli} | 👥 Pax: ${contatorePax}\n============================\n${elencoPrenotazioni}\nBuon lavoro Staff! 💪`; numeriPersonale.forEach(numero => inviaMessaggioDiretto(numero, messaggioFinale)); } // ========================================================= // --- FUNZIONI DI SUPPORTO --- // ========================================================= function getNumeriStaffByRuolo(sheet, ruoloCercato) { const data = sheet.getDataRange().getValues(); let numeri = []; for (let i = 1; i < data.length; i++) { if (String(data[i][2]).trim().toLowerCase() === ruoloCercato.toLowerCase() && String(data[i][1]).length > 5) numeri.push(String(data[i][1])); } return numeri; } function getTemplateFromSheet(sheet, keyName) { const data = sheet.getDataRange().getValues(); for (let i = 0; i < data.length; i++) { if (String(data[i][0]).trim() === keyName) return String(data[i][1]); } return null; } // ============================================================================== // --- FUNZIONE DI INVIO UNIVERSALE (Tutte le colonne A-W mappate) --- // ============================================================================== function inviaMessaggioWA(sheet, rowIndex, colIndexStato, rowData, template, nuovoStato, isStaff, overrideNumero) { try { // 1. RECUPERO E FORMATTAZIONE DATI (Colonna per Colonna) // Colonna A (Indice 0) - ID const id_prenotazione = String(rowData[0] || ''); // Colonna B (Indice 1) - Data Creazione const dataCreaRaw = rowData[1]; const dataCreazione = dataCreaRaw instanceof Date ? Utilities.formatDate(dataCreaRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy HH:mm') : String(dataCreaRaw); // Colonna C (Indice 2) - Data Prenotazione (Evento) const dataPrenRaw = rowData[COL_DATA_PRENOTAZIONE]; const dataEvento = dataPrenRaw instanceof Date ? Utilities.formatDate(dataPrenRaw, Session.getScriptTimeZone(), 'dd/MM/yyyy') : String(dataPrenRaw); // Colonna D (Indice 3) - Nome const nome = String(rowData[COL_NOME] || ''); // Colonna E (Indice 4) - Telefono let telefonoCliente = String(rowData[COL_TELEFONO] || ''); // Colonna F (Indice 5) - Numero Persone const numPersone = String(rowData[COL_NUM_CLIENTI] || ''); // Colonna G (Indice 6) - Pacchetto const pacchetto = String(rowData[COL_PACCHETTO] || ''); // Colonna H (Indice 7) - Costo Animazione const costoAnim = parseFloat(rowData[7] || 0).toFixed(2); // Colonna I (Indice 8) - Extra const extra = parseFloat(rowData[8] || 0).toFixed(2); // Colonna J (Indice 9) - Note const note = String(rowData[9] || ''); // Colonna K (Indice 10) - Richiedi Acconto const richiediAcconto = String(rowData[10] || ''); // Colonna L (Indice 11) - Totale const totale = parseFloat(rowData[11] || 0).toFixed(2); // Colonna M (Indice 12) - Sconto const sconto = parseFloat(rowData[12] || 0).toFixed(2); // Colonna N (Indice 13) - Importo Acconto const acconto = parseFloat(rowData[COL_ACCONTO] || 0).toFixed(2); // Colonna O (Indice 14) - Saldo (Totale da pagare) const saldo = parseFloat(rowData[COL_SALDO] || 0).toFixed(2); // Colonna P (Indice 15) - Stato Pagamento (es. Acconto pagato) const statoPagamento = String(rowData[15] || ''); // Colonna Q (Indice 16) - Link Stripe const link = String(rowData[COL_LINK_STRIPE] || ''); // Colonna R (Indice 17) - Stato Automazione const statoAutomazione = String(rowData[17] || ''); // Colonna S (Indice 18) - Email const email = String(rowData[18] || ''); // Colonna T (Indice 19) - ID Numerico const idNumerico = String(rowData[19] || ''); // Colonna U (Indice 20) - Tipo Prenotazione const tipoPren = String(rowData[20] || ''); // Colonna V (Indice 21) - Descrizione Menu const descMenu = String(rowData[21] || ''); // Colonna W (Indice 22) - Descrizione Animazione const descAnimazione = String(rowData[22] || ''); // 2. DETERMINAZIONE DESTINATARIO E PULIZIA NUMERO let destinatario = isStaff ? overrideNumero : telefonoCliente; destinatario = String(destinatario).replace(/[^\d]/g, ''); if (destinatario.startsWith('00')) destinatario = destinatario.substring(2); if (!destinatario.startsWith('39') && destinatario.length > 5) destinatario = '39' + destinatario; // 3. SOSTITUZIONE MASSIVA DEI PLACEHOLDER // Usa .replace con /g (global) per sostituire tutte le occorrenze nel testo let messaggioFinale = template .replace(/{id_prenotazione}/g, id_prenotazione) .replace(/{data_creazione}/g, dataCreazione) .replace(/{data_evento}/g, dataEvento) .replace(/{nome_cliente}/g, nome) .replace(/{telefono}/g, telefonoCliente) .replace(/{numero_persone}/g, numPersone) .replace(/{pacchetto_scelto}/g, pacchetto) .replace(/{costo_animazione}/g, costoAnim) .replace(/{extra}/g, extra) .replace(/{note}/g, note) .replace(/{richiedi_acconto}/g, richiediAcconto) .replace(/{totale}/g, totale) .replace(/{sconto}/g, sconto) .replace(/{importo_acconto}/g, acconto) .replace(/{saldo}/g, saldo) .replace(/{stato_pagamento}/g, statoPagamento) .replace(/{link_stripe}/g, link) .replace(/{stato_automazione}/g, statoAutomazione) .replace(/{email}/g, email) .replace(/{id_numerico}/g, idNumerico) .replace(/{tipo_prenotazione}/g, tipoPren) .replace(/{descrizione_menu}/g, descMenu) .replace(/{animazione}/g, descAnimazione); // 4. INVIO TRAMITE SENDAPP const payload = { "access_token": WA_TOKEN, "instance_id": WA_INSTANCE, "number": destinatario, "message": messaggioFinale }; const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; const response = UrlFetchApp.fetch(WA_API_URL, options); const result = JSON.parse(response.getContentText()); // 5. GESTIONE ESITO E AGGIORNAMENTO FOGLIO if (result.status === 'success' || result.message === 'Message sent successfully') { if (!isStaff) sheet.getRange(rowIndex, colIndexStato + 1).setValue(nuovoStato); Logger.log(`OK: Messaggio inviato a ${destinatario}`); return true; } else { if (!isStaff) sheet.getRange(rowIndex, colIndexStato + 1).setValue('Errore API'); Logger.log('Errore API: ' + result.message); return false; } } catch (e) { if (!isStaff) sheet.getRange(rowIndex, colIndexStato + 1).setValue('Errore Script'); Logger.log('Errore Critico Script: ' + e.toString()); return false; } }