Gestion des tâches d'une équipe avec Google Tasks

Souvent demandé par les utilisateurs, je vous propose une solution pour gérer les tâches d'une équipe en utilisant Google Sheets et Google Tasks.

L'idée

L'idée est la suivante : saisir dans un Google Sheets différentes tâches d'un projet et pouvoir les envoyer dans "le" Google Tasks de la personne qui a la charge de la tâche.


La réalisation

Pour cela, vous aurez besoin de script avec un menu contenant 4 options:
  • la première option "Go" permettant lancer le traitement ; celui li chaque ligne cochée et envoi la tâche dans le compte indiqué dans la colonne A
  • la seconde option "Connecter" permet à chaque membre de l'équipe de se connecter au script permettant au traitement d'ajouter des tâches dans le Google Tasks de l'email de la colonne A
  • la troisème option "Déconnecter" permet de se déconnecter.
  • la quatrième option "Créer un modèle" plus didactique permet d'obtenir 3 première lignes d'exemple.
Lors du traitement, vous êtes avertis du bon déroulement du script via le messge Succès: envoyé par adresse1@domaine.com

La proposition de script

const CONFIG = {
NAME: "Google Tasks",
TEMPLATE: [
["Envoyer à*", "Titre*", "[Notes]", "[Date d'échéance]", "[Liste]", "Envoyé*", "[Statut]", "[Horodatage]"],
[null, 'Titre de la tâche 1', 'Note Tâche 1', new Date(), "Mes tâches", true, null, null],
[null, 'Titre de la tâche 2', 'Note Tâche 2', new Date(), "Mes tâches", true, null, null],
[null, 'Titre de la tâche 3', 'Note Tâche 3', new Date(), "Mes tâches", true, null, null],
],
SHEET_NAME: {
APP: "Tâches",
},
REFRESH_TOKEN_EVERY_MINUTES: 10,
}

class Utils {
constructor(name="Utils") {
this.name = name
this.ss = SpreadsheetApp.getActive()
}

confirm(message) {
const ui = SpreadsheetApp.getUi()
return ui.alert(`${this.name} [confirm]`, message, ui.ButtonSet.YES_NO)
}

alert(message, type = "warning") {
const ui = SpreadsheetApp.getUi()
return ui.alert(`${this.name} [${type}]`, message, ui.ButtonSet.OK)
}

toast(message, timeoutSeconds = 15) {
return this.ss.toast(message, this.name, timeoutSeconds)
}

valuesToSheet(values, sheetName) {
const ws = this.ss.getSheetByName(sheetName) || this.ss.insertSheet(sheetName)
ws.clear()
ws.getRange(1, 1, values.length, values[0].length).setValues(values)
return ws
}

/**
* @param {Date} date
*/
formatDate(date){
if (typeof date === 'string') date = new Date(date)
return Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd'T'HH:mm:ss'Z'")
}

createTrigger(){
const functionName = "refreshToken"
ScriptApp.getProjectTriggers().forEach(trigger => ScriptApp.deleteTrigger(trigger))
ScriptApp.newTrigger(functionName)
.timeBased()
.everyMinutes(CONFIG.REFRESH_TOKEN_EVERY_MINUTES)
.create()
}
}

class Task {
constructor(user) {
this.user = user
this.utils = new Utils()
this.props = PropertiesService.getScriptProperties()
this.cache = CacheService.getScriptCache()
}

getAllTaskLists(token) {
const url = 'https://tasks.googleapis.com/tasks/v1/users/@me/lists?maxResults=100'
const response = UrlFetchApp.fetch(url, {
method: "GET",
headers: { "Authorization": `Bearer ${token}` },
contentType: 'application/json',
muteHttpExceptions: true
})
const result = JSON.parse(response.getContentText())
if (result.error) {
throw new Error(result.error.message)
}
return result.items
}

getTaskListByName(name, token) {
const cacheKey = `${this.user}:${name}`

const taskLists = this.getAllTaskLists(token)
const foundTaskList = taskLists.find(item => item.title === name)
if (foundTaskList) {
this.cache.put(cacheKey, JSON.stringify(foundTaskList), 21600)
return foundTaskList
}
const url = 'https://tasks.googleapis.com/tasks/v1/users/@me/lists'
const response = UrlFetchApp.fetch(url, {
method: "POST",
headers: { "Authorization": `Bearer ${token}` },
contentType: 'application/json',
payload: JSON.stringify({ title: name }),
muteHttpExceptions: true
})
const item = JSON.parse(response.getContentText())
if (item.error) {
throw new Error(item.error.message)
}
this.cache.put(cacheKey, JSON.stringify(item), 21600)
return item
}

createTask({ title, notes, due, to, taskListName, send, status, timestamp }) {
if (!send) return [send, status, timestamp]
if (!to) return [true, 'Erreur: "Envoyé à" est requis!', new Date()]
if (!title) return [true, 'Erreur: "Titre" est requis!', new Date()]
const token = this.props.getProperty(to)
if (!token) return [true, `Erreur: utilisateur ${to} non connecté à la feuille de calcul.`, new Date()]

taskListName = taskListName || CONFIG.NAME
const taskList = this.getTaskListByName(taskListName, token)
const url = `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks`
notes = notes ? `${notes}\nAssigné par: ${this.user}` : `Assigné par: ${this.user}`
due = this.utils.formatDate(due)
const response = UrlFetchApp.fetch(url, {
method: "POST",
headers: { "Authorization": `Bearer ${token}` },
contentType: 'application/json',
payload: JSON.stringify({ title, notes, due }),
muteHttpExceptions: true
})
const item = JSON.parse(response.getContentText())
if (item.error){
return [true, `Erreur: ${item.error.message}`, new Date()]
}
return [false, `Succès: envoyé par ${this.user}`, new Date()]
}
}

class App {
constructor() {
this.name = CONFIG.NAME
this.user = Session.getActiveUser().getEmail()
this.utils = new Utils(this.name)
this.task = new Task(this.user)
this.ss = SpreadsheetApp.getActive()
this.props = PropertiesService.getScriptProperties()
this.cache = CacheService.getScriptCache()
}

onOpen(e) {
const ui = SpreadsheetApp.getUi()
ui.createMenu(this.name)
.addItem("Go", "run")
.addSeparator()
.addItem("Connecter", "connect")
.addItem("Déconnecter", "disconnect")
.addItem("Créer un modèle", "createTemplate")
.addToUi()
this.formater()
}

getUserEmails() {
return Object.keys(this.props.getProperties()).filter(key => key.includes("@"))
}

updateUserEmailValidations() {
const ws = this.ss.getSheetByName(CONFIG.SHEET_NAME.APP)
if (!ws) return
const lastRow = ws.getLastRow()
if (lastRow < 2) return
const emails = this.getUserEmails()
const dataValidation = SpreadsheetApp.newDataValidation().requireValueInList(emails)
ws.getRange(`A2:A${lastRow}`).setDataValidation(dataValidation)
}

createTemplate() {
const ui = SpreadsheetApp.getUi()
const confirm = this.utils.confirm(`Etes-vous sûr de créer un nouveau modèle dans la feuille "${CONFIG.SHEET_NAME.APP}" ?`)
if (confirm !== ui.Button.YES) return this.utils.toast("Annulé!")
const template = this.utils.valuesToSheet(CONFIG.TEMPLATE, CONFIG.SHEET_NAME.APP)
this.updateUserEmailValidations()
template.getRange("H2:H" + CONFIG.TEMPLATE.length).setNumberFormat("dd/M/yyyy H:mm:ss")
template.getRange("F2:F" + CONFIG.TEMPLATE.length).insertCheckboxes()
template.activate()
this.utils.toast("Un nouveau modèle a été créé !")
}

connect() {
const ui = SpreadsheetApp.getUi()
const confirm = this.utils.confirm(`Êtes-vous sûr de connecter votre compte ${this.user} ?`)
if (confirm !== ui.Button.YES) return this.utils.toast("Annulé !")
this.refreshToken()
this.updateUserEmailValidations()
this.utils.createTrigger()
this.utils.toast("Connecté !")
}

disconnect(){
const ui = SpreadsheetApp.getUi()
const confirm = this.utils.confirm(`Are you sure to disconnect your account ${this.user}?`)
if (confirm !== ui.Button.YES) return this.utils.toast("Annulé!")
this.props.deleteProperty(this.user)
this.updateUserEmailValidations()
ScriptApp.getProjectTriggers().forEach(trigger => ScriptApp.deleteTrigger(trigger))
this.utils.toast("Déconnecté !")
}

formater(){
const ws = this.ss.getSheetByName(CONFIG.SHEET_NAME.APP)
if (!ws) return
const lastRow = ws.getLastRow()
if (lastRow < 2) return
const dataValidation = SpreadsheetApp.newDataValidation().requireDate()
ws.getRange(`D2:D${lastRow}`).setDataValidation(dataValidation)
}

refreshToken(){
const userEmail = Session.getActiveUser().getEmail()
const token = ScriptApp.getOAuthToken()
this.props.setProperty(userEmail, token)
}

run() {
const ws = this.ss.getSheetByName(CONFIG.SHEET_NAME.APP)
if (!ws) {
this.utils.alert(`La feuille "${CONFIG.SHEET_NAME.APP}" introuvable dans le fichier!`, 'Attention')
return this.createTemplate()
}
const [, ...items] = ws.getDataRange().getValues()
const results = items.map(item => {
return this.task.createTask({
to: item[0],
title: item[1],
notes: item[2],
due: item[3],
taskListName: item[4],
send: item[5],
status: item[6],
timestamp: item[7],
})
})
if (results.length) {
ws.getRange(`F2:H${results.length + 1}`).setValues(results)
}
this.utils.toast("Fait !")
}
}

const app = new App()
const onOpen = (e) => app.onOpen(e)
const connect = () => app.connect()
const formater = () => app.formater()
const disconnect = () => app.disconnect()
const run = () => app.run()
const refreshToken = () => app.refreshToken()
const createTemplate = () => app.createTemplate()