Cet article décrit la méthode pour créer un tableau de bord visuel à partir des données d'une table TimeTonic, en utilisant Google Sheets comme moteur de calcul et de visualisation. À chaque création ou modification d'une ligne dans TimeTonic, les données sont synchronisées automatiquement vers Google Sheets via un webhook.
Principe général
Le système repose sur trois briques connectées. Chaque mise à jour côté TimeTonic est répercutée automatiquement vers Google Sheets, qui calcule les statistiques et génère les graphiques affichés dans votre dashboard.
Côté TimeTonic : créer le scénario d'automatisation
Dans votre table TimeTonic, accédez à Automate et créez un nouveau scénario d'automatisation. Suivez ces quatre étapes pour le configurer.
Une ligne créée peut être enrichie progressivement, par exemple via des champs remplis ultérieurement ou des valeurs calculées par d'autres scénarios. Le trigger When a record view is saved capte chaque enregistrement, ce qui garantit que votre Sheet reflète toujours l'état actuel de votre table.
Côté Google Sheets : structurer votre fichier en trois onglets
Créez un Google Sheet et organisez-le en trois onglets distincts. Chacun joue un rôle précis dans la chaîne de traitement des données.
Installer le script Apps Script
Le script Apps Script joue le rôle de réceptionniste entre TimeTonic et Google Sheets. Il reçoit les données envoyées par le webhook et les inscrit dans l'onglet Raw Data, en évitant les doublons grâce à une logique de mise à jour.
Voici l'exemple du script dans Google Sheet.
/**
* TimeTonic → Google Sheet Webhook Receiver
*
* Receives intervention data from TimeTonic automation
* and writes it into the "Raw Data" sheet.
*
* Features:
* - UPSERT logic: 1 row per intervention (updates existing rows, avoids duplicates)
* - Auto-calculates concat keys for COUNTIF/SUMIF formulas
* - Handles FR and US numeric formats (comma and dot decimals)
* - Forces number format on cells (avoids Google Sheets locale issues)
*/
// === CONFIGURATION ===
// Replace with your own Spreadsheet ID (the part between /d/ and /edit in your Sheet URL)
const SPREADSHEET_ID = "1ilnTpPZabmUfFz1o3CC39kYYB4mRQ34Hc8B8ieHja4Q";
const SHEET_NAME = "Raw Data";
// Column where Intervention # is stored (column D = 4th column)
const INTERVENTION_NUM_COL = 4;
// === MAIN FUNCTIONS ===
function getSheet() {
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = spreadsheet.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error("Sheet '" + SHEET_NAME + "' not found");
}
return sheet;
}
/**
* Webhook endpoint called by TimeTonic automation
*/
function doPost(e) {
try {
const sheet = getSheet();
const payload = JSON.parse(e.postData.contents);
// Extract year and month from date (for concat keys)
const dateStr = payload.date || "";
const { year, month } = parseDate(dateStr);
// Build the 3 concat keys used by Stats sheet COUNTIF formulas
const statsKeyType = year && month
? year + "-" + month + "-" + (payload.type || "")
: "";
const statsKeyStatus = year && month
? year + "-" + month + "-" + (payload.status || "")
: "";
const statsKeyTech = year && month
? year + "-" + month + "-" + (payload.technician || "")
: "";
const totalHours = toNumber(payload.total_hours);
const interventionNum = toNumber(payload.intervention_num);
if (!interventionNum && interventionNum !== 0) {
throw new Error("Missing intervention_num in payload");
}
const rowValues = [
dateStr, // A : Date
year, // B : Year
month, // C : Month
interventionNum, // D : Intervention #
payload.title || "", // E : Title
payload.type || "", // F : Type
payload.status || "", // G : Status
payload.customer || "", // H : Customer
payload.site || "", // I : Site
payload.technician || "", // J : Technician
totalHours, // K : Total Hours
statsKeyType, // L : Stats Key Type
statsKeyStatus, // M : Stats Key Status
statsKeyTech // N : Stats Key Tech
];
// UPSERT: search existing row with same Intervention #
const existingRow = findRowByInterventionNum(sheet, interventionNum);
let targetRow, action;
if (existingRow > 0) {
targetRow = existingRow;
action = "updated";
} else {
sheet.insertRowBefore(2);
targetRow = 2;
action = "inserted";
}
// Force number format BEFORE writing (fixes FR locale issue)
sheet.getRange(targetRow, 4).setNumberFormat("0"); // Intervention #
sheet.getRange(targetRow, 11).setNumberFormat("General"); // Total Hours
sheet.getRange(targetRow, 1, 1, rowValues.length).setValues([rowValues]);
return ContentService
.createTextOutput(JSON.stringify({
status: "ok",
action: action,
row: targetRow,
intervention_num: interventionNum
}))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ status: "error", message: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
/**
* Find existing row by Intervention #
* Returns row number (1-indexed) if found, 0 if not found
*/
function findRowByInterventionNum(sheet, interventionNum) {
const lastRow = sheet.getLastRow();
if (lastRow < 2) return 0;
const numCol = sheet.getRange(2, INTERVENTION_NUM_COL, lastRow - 1, 1).getValues();
const searchNum = Number(interventionNum);
for (let i = 0; i < numCol.length; i++) {
if (Number(numCol[i][0]) === searchNum) {
return i + 2;
}
}
return 0;
}
/**
* Convert value to number (handles "2.5", "2,5", 2.5, null, etc.)
*/
function toNumber(val) {
if (val === null || val === undefined || val === "") return "";
if (typeof val === "number") return val;
const cleaned = String(val).trim().replace(",", ".");
const num = parseFloat(cleaned);
return isNaN(num) ? "" : num;
}
/**
* Extract year and 2-digit month from a date string
* Accepts: DD/MM/YYYY, YYYY-MM-DD, DD-MM-YYYY
*/
function parseDate(dateStr) {
if (!dateStr) return { year: "", month: "" };
let match = dateStr.match(/^(\d{4})-(\d{2})-\d{2}/);
if (match) return { year: match[1], month: match[2] };
match = dateStr.match(/^(\d{2})[\/\-](\d{2})[\/\-](\d{4})/);
if (match) return { year: match[3], month: match[2] };
return { year: "", month: "" };
}Construire les tableaux de calcul dans l'onglet Stats
Dans l'onglet Stats, créez des zones de calcul à l'aide de deux fonctions simples qui lisent les données depuis Raw Data : SUMIF et COUNTIF.
SUMIF additionne une valeur en filtrant sur un critère :
COUNTIF compte le nombre d'occurrences d'une valeur dans une colonne :
Construire les graphiques dans l'onglet Dashboard
Pour chaque tableau de calcul créé dans l'onglet Stats, générez un graphique en suivant ces quatre étapes.
Publier le dashboard en mode sécurisé
Trois précautions à prendre dans l'ordre pour protéger vos données sources et exposer uniquement le dashboard.
Intégrer le dashboard dans une Smart Page TimeTonic
Dernière étape : afficher votre tableau de bord directement dans TimeTonic via le module Smart Page.
Aller plus loin
Voir le tutoriel vidéo
La méthode complète en 5 minutes, étape par étape.
Aller plus loin
Maîtriser les automatisations
Tous les déclencheurs et actions disponibles dans TimeTonic.
Aller plus loin
Utiliser les Smart Pages
Intégrer des contenus externes dans votre espace TimeTonic.