/***********/***********************
* PLANNING PRO PERSONNEL
* Semaine (time blocking 30 min), Journal, Stats, Paramètres
* Langue: FR. Fuseau: Africa/Casablanca par défaut de ton compte.
***********************/
const CONFIG = {
sheetNames: {
week: "Semaine",
journal: "Journal",
stats: "Stats",
settings: "Paramètres"
},
startHour: 6, // débute à 06:00
endHour: 23, // termine à 23:00
slotMinutes: 30, // pas de 30 minutes
days: ["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"],
defaultCategories: [
["Travail","#1a73e8"],
["Sport","#34a853"],
["Déjeuner","#fbbc05"],
["Sortie","#a142f4"],
["Étude / Focus","#00bcd4"],
["Repos","#9aa0a6"],
["Divers","#ff7043"]
]
};
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("Planning")
.addItem("Nouvelle semaine", "resetWeekToNextMonday")
.addItem("Exporter la semaine vers Journal", "exportWeekToJournal")
.addItem("Recolorer d'après Paramètres", "applyWeekConditionalFormatting")
.addToUi();
}
// Lance une fois au début
function setupTemplate() {
const ss = SpreadsheetApp.getActive();
// Crée/vides feuilles
const shSettings = getOrCreateSheet_(CONFIG.sheetNames.settings, ss);
const shWeek = getOrCreateSheet_(CONFIG.sheetNames.week, ss);
const shJournal = getOrCreateSheet_(CONFIG.sheetNames.journal, ss);
const shStats = getOrCreateSheet_(CONFIG.sheetNames.stats, ss);
setupSettings_(shSettings);
setupWeek_(shWeek, shSettings);
setupJournal_(shJournal);
setupStats_(shStats, shJournal, shSettings);
// Esthétique minimale
ss.setSpreadsheetLocale("fr_FR");
ss.rename("Planning personnel");
SpreadsheetApp.getUi().alert("Planning prêt. Menu « Planning » ajouté. Remplis la feuille Semaine, puis exporte vers le Journal.");
}
/* ---------------- Paramètres ---------------- */
function setupSettings_(sh) {
sh.clear();
sh.setTabColor("#263238");
sh.getRange("A1").setValue("Catégorie");
sh.getRange("B1").setValue("Couleur (hex)");
sh.getRange("D1").setValue("Infos");
sh.getRange("D2").setValue("Ajoute/édite tes catégories ici. Les couleurs pilotent la mise en forme.");
sh.getRange("A1:B1").setFontWeight("bold");
sh.setColumnWidths(1, 2, 160);
sh.setColumnWidths(4, 1, 380);
// Valeurs par défaut si vide
const dataRange = sh.getRange(2,1,CONFIG.defaultCategories.length,2);
dataRange.setValues(CONFIG.defaultCategories);
// Nom de plage pour validation
const lastRow = 1 + CONFIG.defaultCategories.length + 50; // marge
sh.getRange(2,1,lastRow,1).createNamedRange("CATEGORIES_LIST");
sh.getRange(2,1,lastRow,2).createNamedRange("CATEGORIES_COLORS");
}
/* ---------------- Semaine (time blocking) ---------------- */
function setupWeek_(sh, shSettings) {
sh.clear();
sh.setTabColor("#0b8043");
const header = ["Heure"].concat(CONFIG.days);
sh.getRange(1,1,1,header.length).setValues([header]).setFontWeight("bold");
sh.getRange("A1:H1").setBackground("#E8F0FE");
// Heures en première colonne
const times = buildTimeSlots_();
sh.getRange(2,1,times.length,1).setValues(times.map(t => [t]));
sh.getRange(2,1,times.length,1).setNumberFormat("HH:mm");
// Grille
const numCols = 1 + CONFIG.days.length;
const numRows = 1 + times.length;
sh.getRange(1,1,numRows,numCols).setBorder(true,true,true,true,true,true);
sh.setFrozenRows(1);
sh.setFrozenColumns(1);
sh.setColumnWidth(1, 70);
for (let c=2; c<=numCols; c++) sh.setColumnWidth(c, 150);
// Validation: liste de catégories
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(SpreadsheetApp.getActive().getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange(2,2,times.length,CONFIG.days.length).setDataValidation(valid);
// Bandeau semaine: lundi → dimanche
const monday = getNextMonday_(new Date()); // base
sh.getRange("J1").setValue("Semaine du");
sh.getRange("K1").setValue(monday);
sh.getRange("K1").setNumberFormat("dd/mm/yyyy").setFontWeight("bold");
sh.getRange("J1:K1").setBackground("#F1F3F4");
// MEP conditionnelle selon couleurs
applyWeekConditionalFormatting();
}
function resetWeekToNextMonday() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(CONFIG.sheetNames.week);
if (!sh) return;
// Vide la grille
const timesLen = buildTimeSlots_().length;
sh.getRange(2,2,timesLen,CONFIG.days.length).clearContent().clearFormat();
// Rappel validations
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(ss.getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange(2,2,timesLen,CONFIG.days.length).setDataValidation(valid);
// Remet couleurs
applyWeekConditionalFormatting();
// Date lundi prochain
sh.getRange("K1").setValue(getNextMonday_(new Date())).setNumberFormat("dd/mm/yyyy");
}
/* Mise en forme conditionnelle basée sur Paramètres */
function applyWeekConditionalFormatting() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(CONFIG.sheetNames.week);
const shSettings = ss.getSheetByName(CONFIG.sheetNames.settings);
if (!sh || !shSettings) return;
const timesLen = buildTimeSlots_().length;
const range = sh.getRange(2,2,timesLen,CONFIG.days.length);
// Efface règles existantes
sh.clearConditionalFormatRules();
// Récupère catégories + couleurs
const catColors = getCategoriesColors_();
const rules = [];
catColors.forEach(([cat,color]) => {
if (!cat || !color) return;
rules.push(
SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(cat)
.setBackground(color)
.setFontColor("#ffffff")
.setRanges([range])
.build()
);
});
// Règle fallback pour cellules non vides
rules.push(
SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("")
.setBackground("#e0e0e0")
.setRanges([range])
.build()
);
sh.setConditionalFormatRules(rules);
}
/* ---------------- Journal ---------------- */
function setupJournal_(sh) {
sh.clear();
sh.setTabColor("#7b1fa2");
const header = ["Date","Début","Fin","Durée (h)","Catégorie","Note"];
sh.getRange(1,1,1,header.length).setValues([header]).setFontWeight("bold").setBackground("#FCE8FF");
sh.setFrozenRows(1);
sh.setColumnWidths(1,6,140);
// Formats
sh.getRange("A2:A").setNumberFormat("dd/mm/yyyy");
sh.getRange("B2:C").setNumberFormat("HH:mm");
sh.getRange("D2:D").setNumberFormat("0.00");
// Validation catégorie
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(SpreadsheetApp.getActive().getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange("E2:E").setDataValidation(valid);
}
/* Exporte la semaine vers lignes Journal, en fusionnant les créneaux contigus d’une même catégorie */
function exportWeekToJournal() {
const ss = SpreadsheetApp.getActive();
const shWeek = ss.getSheetByName(CONFIG.sheetNames.week);
const shJournal = ss.getSheetByName(CONFIG.sheetNames.journal);
if (!shWeek || !shJournal) throw new Error("Feuille manquante.");
const times = buildTimeSlots_(); // array de Date objets heures du jour fictif
const grid = shWeek.getRange(2,2,times.length,CONFIG.days.length).getValues();
const monday = shWeek.getRange("K1").getValue(); // base semaine
const entries = [];
for (let d=0; d<CONFIG.days.length; d++) {
let currentCat = null;
let startIdx = null;
for (let r=0; r<times.length; r++) {
const cellVal = (grid[r][d] || "").toString().trim();
const isChange = cellVal !== currentCat;
if (isChange) {
if (currentCat) {
// fermer période précédente
entries.push(buildEntry_(monday, d, times, startIdx, r, currentCat));
}
currentCat = cellVal || null;
startIdx = currentCat ? r : null;
}
}
// Fin de journée
if (currentCat) {
entries.push(buildEntry_(monday, d, times, startIdx, times.length, currentCat));
}
}
if (entries.length === 0) {
SpreadsheetApp.getUi().alert("Aucune cellule renseignée dans Semaine.");
return;
}
// Écrit dans Journal
const startRow = shJournal.getLastRow() + 1;
shJournal.getRange(startRow,1,entries.length,entries[0].length).setValues(entries);
SpreadsheetApp.getUi().alert("Export terminé: " + entries.length + " lignes ajoutées au Journal.");
}
function buildEntry_(mondayDate, dayOffset, times, startIdx, endIdx, cat) {
const date = new Date(mondayDate);
date.setDate(date.getDate() + dayOffset);
const start = times[startIdx];
const end = times[endIdx] || addMinutes_(times[times.length-1], CONFIG.slotMinutes);
// Convert HH:mm à date du jour
const dStart = new Date(date); dStart.setHours(start.getHours(), start.getMinutes(), 0, 0);
const dEnd = new Date(date); dEnd.setHours(end.getHours(), end.getMinutes(), 0, 0);
const hours = (dEnd - dStart) / 1000 / 3600;
return [
new Date(date),
new Date(dStart),
new Date(dEnd),
Number(hours.toFixed(2)),
cat,
"" // Note vide
];
}
/* ---------------- Stats ---------------- */
function setupStats_(sh, shJournal, shSettings) {
sh.clear();
sh.setTabColor("#e37400");
sh.getRange("A1").setValue("Stats hebdomadaires par catégorie").setFontWeight("bold").setFontSize(12);
sh.getRange("A1").setBackground("#FFF3E0");
// Période: semaine de la feuille Semaine si dispo, sinon semaine courante
sh.getRange("A3").setValue("Lundi de la semaine");
sh.getRange("B3").setFormula('=IFERROR(\'Semaine\'!K1,TO_DATE(ROUNDDOWN((TODAY()-2)/7)*7+2))'); // lundi européen approx
sh.getRange("A4").setValue("Dimanche de la semaine");
sh.getRange("B4").setFormula('=B3+6');
// Tableau catégories
sh.getRange("A6").setValue("Catégorie").setFontWeight("bold");
sh.getRange("B6").setValue("Heures").setFontWeight("bold");
sh.getRange("A6:B6").setBackground("#FFE0B2");
// Recopie dynamique des catégories depuis Paramètres
sh.getRange("A7").setFormula('=FILTER(Paramètres!A2:A,Paramètres!A2:A<>"")');
// Heures par catégorie (SUMIFS sur Journal entre B3 et B4)
sh.getRange("B7").setFormula(
'=ARRAYFORMULA(IF(A7:A="",,ROUND(SUMIFS(Journal!D:D,Journal!E:E,A7:A,Journal!A:A,">="&$B$3,Journal!A:A,"<="&$B$4),2)))'
);
// Petit donut
const lastRow = 6 + 1 + CONFIG.defaultCategories.length + 20;
const chart = sh.newChart()
.setChartType(Charts.ChartType.PIE)
.addRange(sh.getRange(6,1,lastRow,2))
.setOption("title", "Répartition hebdo (heures)")
.setPosition(2,4,0,0)
.build();
sh.insertChart(chart);
}
/* ---------------- Helpers ---------------- */
function getOrCreateSheet_(name, ss) {
let sh = ss.getSheetByName(name);
if (!sh) sh = ss.insertSheet(name);
return sh;
}
function buildTimeSlots_() {
const arr = [];
const base = new Date(2000,0,1,CONFIG.startHour,0,0,0);
const end = new Date(2000,0,1,CONFIG.endHour,0,0,0);
let cur = new Date(base);
while (cur <= end) {
arr.push(new Date(cur));
cur = addMinutes_(cur, CONFIG.slotMinutes);
}
return arr;
}
function addMinutes_(date, m) {
const d = new Date(date);
d.setMinutes(d.getMinutes()+m);
return d;
}
function getNextMonday_(d) {
const date = new Date(d);
const day = date.getDay(); // 0=dim
const diff = (day === 1) ? 0 : ((8 - day) % 7);
date.setHours(0,0,0,0);
date.setDate(date.getDate() + diff);
return date;
}
function getCategoriesColors_() {
const ss = SpreadsheetApp.getActive();
const range = ss.getRangeByName("CATEGORIES_COLORS");
const values = range.getValues().filter(r => r[0] && r[1]);
return values; // [[cat, color], ...]
}
************
* PLANNING PRO PERSONNEL
* Semaine (time blocking 30 min), Journal, Stats, Paramètres
* Langue: FR. Fuseau: Africa/Casablanca par défaut de ton compte.
***********************/
const CONFIG = {
sheetNames: {
week: "Semaine",
journal: "Journal",
stats: "Stats",
settings: "Paramètres"
},
startHour: 6, // débute à 06:00
endHour: 23, // termine à 23:00
slotMinutes: 30, // pas de 30 minutes
days: ["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"],
defaultCategories: [
["Travail","#1a73e8"],
["Sport","#34a853"],
["Déjeuner","#fbbc05"],
["Sortie","#a142f4"],
["Étude / Focus","#00bcd4"],
["Repos","#9aa0a6"],
["Divers","#ff7043"]
]
};
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("Planning")
.addItem("Nouvelle semaine", "resetWeekToNextMonday")
.addItem("Exporter la semaine vers Journal", "exportWeekToJournal")
.addItem("Recolorer d'après Paramètres", "applyWeekConditionalFormatting")
.addToUi();
}
// Lance une fois au début
function setupTemplate() {
const ss = SpreadsheetApp.getActive();
// Crée/vides feuilles
const shSettings = getOrCreateSheet_(CONFIG.sheetNames.settings, ss);
const shWeek = getOrCreateSheet_(CONFIG.sheetNames.week, ss);
const shJournal = getOrCreateSheet_(CONFIG.sheetNames.journal, ss);
const shStats = getOrCreateSheet_(CONFIG.sheetNames.stats, ss);
setupSettings_(shSettings);
setupWeek_(shWeek, shSettings);
setupJournal_(shJournal);
setupStats_(shStats, shJournal, shSettings);
// Esthétique minimale
ss.setSpreadsheetLocale("fr_FR");
ss.rename("Planning personnel");
SpreadsheetApp.getUi().alert("Planning prêt. Menu « Planning » ajouté. Remplis la feuille Semaine, puis exporte vers le Journal.");
}
/* ---------------- Paramètres ---------------- */
function setupSettings_(sh) {
sh.clear();
sh.setTabColor("#263238");
sh.getRange("A1").setValue("Catégorie");
sh.getRange("B1").setValue("Couleur (hex)");
sh.getRange("D1").setValue("Infos");
sh.getRange("D2").setValue("Ajoute/édite tes catégories ici. Les couleurs pilotent la mise en forme.");
sh.getRange("A1:B1").setFontWeight("bold");
sh.setColumnWidths(1, 2, 160);
sh.setColumnWidths(4, 1, 380);
// Valeurs par défaut si vide
const dataRange = sh.getRange(2,1,CONFIG.defaultCategories.length,2);
dataRange.setValues(CONFIG.defaultCategories);
// Nom de plage pour validation
const lastRow = 1 + CONFIG.defaultCategories.length + 50; // marge
sh.getRange(2,1,lastRow,1).createNamedRange("CATEGORIES_LIST");
sh.getRange(2,1,lastRow,2).createNamedRange("CATEGORIES_COLORS");
}
/* ---------------- Semaine (time blocking) ---------------- */
function setupWeek_(sh, shSettings) {
sh.clear();
sh.setTabColor("#0b8043");
const header = ["Heure"].concat(CONFIG.days);
sh.getRange(1,1,1,header.length).setValues([header]).setFontWeight("bold");
sh.getRange("A1:H1").setBackground("#E8F0FE");
// Heures en première colonne
const times = buildTimeSlots_();
sh.getRange(2,1,times.length,1).setValues(times.map(t => [t]));
sh.getRange(2,1,times.length,1).setNumberFormat("HH:mm");
// Grille
const numCols = 1 + CONFIG.days.length;
const numRows = 1 + times.length;
sh.getRange(1,1,numRows,numCols).setBorder(true,true,true,true,true,true);
sh.setFrozenRows(1);
sh.setFrozenColumns(1);
sh.setColumnWidth(1, 70);
for (let c=2; c<=numCols; c++) sh.setColumnWidth(c, 150);
// Validation: liste de catégories
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(SpreadsheetApp.getActive().getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange(2,2,times.length,CONFIG.days.length).setDataValidation(valid);
// Bandeau semaine: lundi → dimanche
const monday = getNextMonday_(new Date()); // base
sh.getRange("J1").setValue("Semaine du");
sh.getRange("K1").setValue(monday);
sh.getRange("K1").setNumberFormat("dd/mm/yyyy").setFontWeight("bold");
sh.getRange("J1:K1").setBackground("#F1F3F4");
// MEP conditionnelle selon couleurs
applyWeekConditionalFormatting();
}
function resetWeekToNextMonday() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(CONFIG.sheetNames.week);
if (!sh) return;
// Vide la grille
const timesLen = buildTimeSlots_().length;
sh.getRange(2,2,timesLen,CONFIG.days.length).clearContent().clearFormat();
// Rappel validations
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(ss.getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange(2,2,timesLen,CONFIG.days.length).setDataValidation(valid);
// Remet couleurs
applyWeekConditionalFormatting();
// Date lundi prochain
sh.getRange("K1").setValue(getNextMonday_(new Date())).setNumberFormat("dd/mm/yyyy");
}
/* Mise en forme conditionnelle basée sur Paramètres */
function applyWeekConditionalFormatting() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(CONFIG.sheetNames.week);
const shSettings = ss.getSheetByName(CONFIG.sheetNames.settings);
if (!sh || !shSettings) return;
const timesLen = buildTimeSlots_().length;
const range = sh.getRange(2,2,timesLen,CONFIG.days.length);
// Efface règles existantes
sh.clearConditionalFormatRules();
// Récupère catégories + couleurs
const catColors = getCategoriesColors_();
const rules = [];
catColors.forEach(([cat,color]) => {
if (!cat || !color) return;
rules.push(
SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(cat)
.setBackground(color)
.setFontColor("#ffffff")
.setRanges([range])
.build()
);
});
// Règle fallback pour cellules non vides
rules.push(
SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("")
.setBackground("#e0e0e0")
.setRanges([range])
.build()
);
sh.setConditionalFormatRules(rules);
}
/* ---------------- Journal ---------------- */
function setupJournal_(sh) {
sh.clear();
sh.setTabColor("#7b1fa2");
const header = ["Date","Début","Fin","Durée (h)","Catégorie","Note"];
sh.getRange(1,1,1,header.length).setValues([header]).setFontWeight("bold").setBackground("#FCE8FF");
sh.setFrozenRows(1);
sh.setColumnWidths(1,6,140);
// Formats
sh.getRange("A2:A").setNumberFormat("dd/mm/yyyy");
sh.getRange("B2:C").setNumberFormat("HH:mm");
sh.getRange("D2:D").setNumberFormat("0.00");
// Validation catégorie
const valid = SpreadsheetApp.newDataValidation()
.requireValueInRange(SpreadsheetApp.getActive().getRangeByName("CATEGORIES_LIST"), true)
.setAllowInvalid(false).build();
sh.getRange("E2:E").setDataValidation(valid);
}
/* Exporte la semaine vers lignes Journal, en fusionnant les créneaux contigus d’une même catégorie */
function exportWeekToJournal() {
const ss = SpreadsheetApp.getActive();
const shWeek = ss.getSheetByName(CONFIG.sheetNames.week);
const shJournal = ss.getSheetByName(CONFIG.sheetNames.journal);
if (!shWeek || !shJournal) throw new Error("Feuille manquante.");
const times = buildTimeSlots_(); // array de Date objets heures du jour fictif
const grid = shWeek.getRange(2,2,times.length,CONFIG.days.length).getValues();
const monday = shWeek.getRange("K1").getValue(); // base semaine
const entries = [];
for (let d=0; d<CONFIG.days.length; d++) {
let currentCat = null;
let startIdx = null;
for (let r=0; r<times.length; r++) {
const cellVal = (grid[r][d] || "").toString().trim();
const isChange = cellVal !== currentCat;
if (isChange) {
if (currentCat) {
// fermer période précédente
entries.push(buildEntry_(monday, d, times, startIdx, r, currentCat));
}
currentCat = cellVal || null;
startIdx = currentCat ? r : null;
}
}
// Fin de journée
if (currentCat) {
entries.push(buildEntry_(monday, d, times, startIdx, times.length, currentCat));
}
}
if (entries.length === 0) {
SpreadsheetApp.getUi().alert("Aucune cellule renseignée dans Semaine.");
return;
}
// Écrit dans Journal
const startRow = shJournal.getLastRow() + 1;
shJournal.getRange(startRow,1,entries.length,entries[0].length).setValues(entries);
SpreadsheetApp.getUi().alert("Export terminé: " + entries.length + " lignes ajoutées au Journal.");
}
function buildEntry_(mondayDate, dayOffset, times, startIdx, endIdx, cat) {
const date = new Date(mondayDate);
date.setDate(date.getDate() + dayOffset);
const start = times[startIdx];
const end = times[endIdx] || addMinutes_(times[times.length-1], CONFIG.slotMinutes);
// Convert HH:mm à date du jour
const dStart = new Date(date); dStart.setHours(start.getHours(), start.getMinutes(), 0, 0);
const dEnd = new Date(date); dEnd.setHours(end.getHours(), end.getMinutes(), 0, 0);
const hours = (dEnd - dStart) / 1000 / 3600;
return [
new Date(date),
new Date(dStart),
new Date(dEnd),
Number(hours.toFixed(2)),
cat,
"" // Note vide
];
}
/* ---------------- Stats ---------------- */
function setupStats_(sh, shJournal, shSettings) {
sh.clear();
sh.setTabColor("#e37400");
sh.getRange("A1").setValue("Stats hebdomadaires par catégorie").setFontWeight("bold").setFontSize(12);
sh.getRange("A1").setBackground("#FFF3E0");
// Période: semaine de la feuille Semaine si dispo, sinon semaine courante
sh.getRange("A3").setValue("Lundi de la semaine");
sh.getRange("B3").setFormula('=IFERROR(\'Semaine\'!K1,TO_DATE(ROUNDDOWN((TODAY()-2)/7)*7+2))'); // lundi européen approx
sh.getRange("A4").setValue("Dimanche de la semaine");
sh.getRange("B4").setFormula('=B3+6');
// Tableau catégories
sh.getRange("A6").setValue("Catégorie").setFontWeight("bold");
sh.getRange("B6").setValue("Heures").setFontWeight("bold");
sh.getRange("A6:B6").setBackground("#FFE0B2");
// Recopie dynamique des catégories depuis Paramètres
sh.getRange("A7").setFormula('=FILTER(Paramètres!A2:A,Paramètres!A2:A<>"")');
// Heures par catégorie (SUMIFS sur Journal entre B3 et B4)
sh.getRange("B7").setFormula(
'=ARRAYFORMULA(IF(A7:A="",,ROUND(SUMIFS(Journal!D:D,Journal!E:E,A7:A,Journal!A:A,">="&$B$3,Journal!A:A,"<="&$B$4),2)))'
);
// Petit donut
const lastRow = 6 + 1 + CONFIG.defaultCategories.length + 20;
const chart = sh.newChart()
.setChartType(Charts.ChartType.PIE)
.addRange(sh.getRange(6,1,lastRow,2))
.setOption("title", "Répartition hebdo (heures)")
.setPosition(2,4,0,0)
.build();
sh.insertChart(chart);
}
/* ---------------- Helpers ---------------- */
function getOrCreateSheet_(name, ss) {
let sh = ss.getSheetByName(name);
if (!sh) sh = ss.insertSheet(name);
return sh;
}
function buildTimeSlots_() {
const arr = [];
const base = new Date(2000,0,1,CONFIG.startHour,0,0,0);
const end = new Date(2000,0,1,CONFIG.endHour,0,0,0);
let cur = new Date(base);
while (cur <= end) {
arr.push(new Date(cur));
cur = addMinutes_(cur, CONFIG.slotMinutes);
}
return arr;
}
function addMinutes_(date, m) {
const d = new Date(date);
d.setMinutes(d.getMinutes()+m);
return d;
}
function getNextMonday_(d) {
const date = new Date(d);
const day = date.getDay(); // 0=dim
const diff = (day === 1) ? 0 : ((8 - day) % 7);
date.setHours(0,0,0,0);
date.setDate(date.getDate() + diff);
return date;
}
function getCategoriesColors_() {
const ss = SpreadsheetApp.getActive();
const range = ss.getRangeByName("CATEGORIES_COLORS");
const values = range.getValues().filter(r => r[0] && r[1]);
return values; // [[cat, color], ...]
}