// 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;
}
Grazie per aver prenotato al Disco Penelope!
La tua richiesta di prenotazione è stata inviata con successo. Riceverai presto un messaggio di conferma su WhatsApp con i dettagli per il versamento dell'acconto (se richiesto).
Per comunicazioni non esitare a contattare il +393409707900
Continua la navigazione// ===========================================
// --- 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;
}
}