Projet Google Sheets + Apps Script – interface Maintenance BT
Bonjour à tous,
Je travaille sur un projet interne de gestion des interventions techniques (Maintenance BT, L.R., Devis) à l’aide de Google Sheets et Google Apps Script.
📋 Description du projet :
Créer une solution automatisée, ergonomique et centralisée pour gérer toutes les interventions liées à la maintenance réseau Basse Tension (B.T.), Lignes Réclamées (L.R.) et Devis techniques via Google Sheets et Google Apps Script.
Interface utilisateur :
- Formulaire HTML modale avec sections :
- 🔎 Filtres de recherche (avec bouton recherche)
- 🗂️ Liste d'interventions (Grid si possible)
- 📄 Formulaire de détails (comme image )
- 🔘 Boutons : Fermer, Exporter, Supprimer, Nouveau, Enregistrer
🙏 plan structuré :
- ---
1. Informations Générales
- Nom du classeur Google Sheets : Gestion des interventions Maintenance BT
BD_Maintenance_SUIVI_(B.T./L.R./DEVIS) → données principales
Paramètres → listes déroulantes (origine, type travaux, équipe, etc.)
Suivi 2025 → Nom du tableau des données
---
2. Types de Champs et Détails dans tableau :
- le tableau Suivi 2025 de l'onglet : BD_Maintenance_SUIVI_(B.T./L.R./DEVIS) (cellule départ : A1)
| Champ : Type de Champ |
ID : Numero Automatique
ORIGINE : liste deroulent depuis une plage (Origine[Origine])
Type Travaux : liste deroulent depuis une plage (TYPE_TRAVAUX[TYPE TRAVAUX])
Urgent : case à coché
Demande Terrassement : case à coché
Bon Sortie 1T45XZT0096 : texte
Bon Retour 1T45XZT0096 : texte
Bon Sortie : texte
Bon Retour : texte
B.T : texte
B. I : texte
N° FICHE DECLAR INC : texte
L.R : texte
Devis : texte
CODE Imputation : texte
TYPE MEDIA : liste deroulent depuis une plage
EQUIPE AMENDIS : liste deroulent depuis une plage
Tournnée : texte
Type Maintenance : liste deroulent depuis une plage (TYPE_MAINTENANCE[TYPE MAINTENANCE])
TYPE RESEAU : liste deroulent depuis une plage (TYPE_RESEAU[TYPE RESEAU])
Expéditeur : texte
Objet : liste deroulent depuis une plage (LISTE_OBJET[Liste_Objet])
LIEUX DES TRAVAUX : texte
Date BT : date
DATE RECUE : date
DATE PROGRAMME : date
DATE EXECUTION : formule calcule = date execution - date BT
Date Répons : date
S / H Tension : liste deroulent depuis une plage (TYPE_TR_TENSION[TYPE_TR_TENSION])
Durée coupure : HEURE
Délai Maintenance : formule calcule = date execution - date BT
EQUIPE Prestataire : liste deroulent depuis une plage (Equipe_Prestataire[Equipe Prestataire])
ACTION RÉALISER : texte
N° BI déclaration waterp : texte
Date Réclamation WhatsApp : date
Agents Depan/Maint/Perm : texte
Agent Juridique Avisé : finances
Main D'ouvre (Par Heures) : HEURE
N° ATTACHEMENT : texte
Date ATTACHEMENT : DATE
Prix ATTACHEMENT : finances
OBSERVATION : TEXTE
---
3. les tableaux des Listes Déroulantes dans l'onglet : Paramétres
**Tableau :TYPE_MEDIA[TYPE MEDIA] ** (cellule depart A1)
- ---
- BT-BON TRAVAIL
- CO-COURRIER
- FA-FAX
- IN-INTERNET
- ML-MAIL
- SM-SMS
- TE-TELEPHONE
- TW-TWITTER
- VS-VISITE DE SITE
- WAT.ERP
**Tableau :LES_EQUIPES[Num Équipes] (contiet: Order; Matricule; N. P. Agent; Code Agent; Réf Équipes** (cellule depart C1) la colon utilisé :
- EQ_Agent01
- Etc...
**Tableau :TYPE_TRAVAUX[TYPE TRAVAUX] ** (cellule depart I1)
- DEGAT
- DEVIS DEPLACEMENT
- DEVIS REMISE A NIVEAU
- L. RECLAMATION
- TRAVAUX MAINTENANCE
**Tableau :TYPE_MAINTENANCE[TYPE MAINTENANCE] ** (cellule depart K1)
- ALIM. EVENEMENT
- ALIM. PROVISOIRE
- CORRECTIVE
- OUVERTURE POSTE
- PREVENTIVE
**Tableau :TYPE_RESEAU[TYPE RESEAU] ** (cellule depart M1)
- EMERGENTE
- MIXTE
- POSTE DP
- SOUTERRAIN
- TORSADE
**Tableau :Origine[Origine]** (cellule depart O1)
- ADMINISTRATION
- D.E.A
- ENT. BRANCHEMENT
- ENT. CONTRÔLE TECHNIQUE
- ENT. DECOUPAGE
- ENT. DEPANNAGE BT
- ENT. ETUDE
- ENT. MAINTENANCE BT
- ENT. MAINT. POSTE
- ENT. TRAVAUX
**Tableau :LISTE_OBJET[Liste_Objet]** (cellule depart y1)
TYPE RESEAU Liste_Objet
ÉMERGENTE COFFRET DE PASSAGE À DÉPLACER
ÉMERGENTE COFFRET DE PASSAGE À POSER
ÉMERGENTE COFFRET DE PASSAGE BRÛLÉ
ÉMERGENTE COFFRET DE PASSAGE CASSÉ
ÉMERGENTE COFFRET DE PASSAGE DÉTACHÉ
ÉMERGENTE COFFRET DE PASSAGE ENDOMMAGÉ
ÉMERGENTE COFFRET DE PASSAGE ENTERRÉ
ÉMERGENTE COFFRET DE PASSAGE N'EXISTE PAS
ÉMERGENTE COFFRET DE PASSAGE SANS GRILLE
ÉMERGENTE COFFRET DE PASSAGE SANS PORTE
ÉMERGENTE PIQUETS DE TERRE
ÉMERGENTE PLOT DE RACCORDEMENT BRÛLÉ
ÉMERGENTE PLOT DE RACCORDEMENT DESSERRÉ
MIXTE ALIMENTATION PROVISOIRE
MIXTE BRANCHEMENT AÉRO-SOUTERRAIN ENDOMMAGÉ
MIXTE CHUTE DE TENSION
MIXTE CONSIGNATION B.T
MIXTE DÉCOUPAGE BT
MIXTE ÉLIMINATION FRAUDE
MIXTE PHASE À LA TERRE
MIXTE RENFORCEMENT PERMANENCE
MIXTE VÉRIFICATION RÉSEAU MIXTE
POSTE DP ÉLIMINATION DES POINTS CHAUDS
POSTE DP FAISCEAUX AVARIÉ
POSTE DP FAISCEAUX BRÛLÉ
POSTE DP NOUVEAU FAISCEAUX (E.P)
POSTE DP POSTE DP VOLÉ
SOUTERRAIN BRANCHEMENT SOUTERRAIN AVARIÉ
SOUTERRAIN BRANCHEMENT SOUTERRAIN BRÛLÉ
SOUTERRAIN BRANCHEMENT SOUTERRAIN COUPÉ
SOUTERRAIN BRANCHEMENT SOUTERRAIN VÉTUSTE
SOUTERRAIN BRANCHEMENT SOUTERRAIN VOLÉ
SOUTERRAIN CÂBLE SOUTERRAIN AVARIÉ
SOUTERRAIN CÂBLE SOUTERRAIN BRÛLÉ
SOUTERRAIN CÂBLE SOUTERRAIN COUPÉ
SOUTERRAIN CÂBLE SOUTERRAIN ENDOMMAGÉ
SOUTERRAIN CÂBLE SOUTERRAIN ENTERRÉ
SOUTERRAIN VÉRIFICATION RÉSEAU SOUTERRAIN
TORSADE BOITE D.A.S BRÛLÉ
TORSADE BOITE J.A.S BRÛLÉ
TORSADE BRANCHEMENT TORSADÉ À DÉPLACER
TORSADE BRANCHEMENT TORSADÉ À ÉLIMINER
TORSADE BRANCHEMENT TORSADÉ BRÛLÉ
TORSADE BRANCHEMENT TORSADÉ COUPÉ
TORSADE BRANCHEMENT TORSADÉ DÉBRANCHÉ
TORSADE BRANCHEMENT TORSADÉ DÉCROCHÉ
TORSADE BRANCHEMENT TORSADÉ ENDOMMAGÉ
TORSADE BRANCHEMENT TORSADÉ PAR TERRE
TORSADE BRANCHEMENT TORSADÉ VÉTUSTE
TORSADE BRANCHEMENT TORSADÉ VOLÉ
TORSADE CÂBLE TORSADÉ À ÉLIMINER
TORSADE CÂBLE TORSADÉ AVARIÉ
TORSADE CÂBLE TORSADÉ BRÛLÉ
TORSADE CÂBLE TORSADÉ COUPÉ
TORSADE CÂBLE TORSADÉ DÉBRANCHÉ
TORSADE CÂBLE TORSADÉ DÉCROCHÉ
TORSADE CÂBLE TORSADÉ ENDOMMAGÉ
TORSADE CÂBLE TORSADÉ PAR TERRE
TORSADE CÂBLE TORSADÉ SUR FENÊTRE
TORSADE CÂBLE TORSADÉ SUR MAISON
TORSADE CÂBLE TORSADÉ VOLÉ
TORSADE DISJONCTEUR TYPE POTEAU AVARIÉ
TORSADE ÉLAGAGE DES ARBRES
TORSADE INSTALLATION D'ANALYSEUR RÉSEAU TORSADÉ
TORSADE POTEAUX BOIS ET BÉTON ET CÂBLE
TORSADE VÉRIFICATION RÉSEAU TORSADÉ
TORSADE VISITE POUR DÉMOLITION
**Tableau :TYPE_TR_TENSION[TYPE_TR_TENSION]** (cellule depart w1)
- S. T.
- H. T
**Tableau :Equipe_Prestataire[Equipe Prestataire]** (cellule depart Q1)
-EQ_prestataire01
-Etc...
**Tableau :AGENT_JURIDIQUE[AGENT JURIDIQUE]** (cellule depart U1)
-Agent_Juridique01
-Etc...
4- les champs de recherche sont (Champ: Type de Champ):
ID : Numero Automatique
ORIGINE : liste deroulent depuis une plage (Origine[Origine])
Type Travaux : liste deroulent depuis une plage (TYPE_TRAVAUX[TYPE TRAVAUX])
Urgent : case à coché
Demande Terrassement : case à coché
Bon Sortie 1T45XZT0096 : texte
Bon Retour 1T45XZT0096 : texte
Bon Sortie : texte
Bon Retour : texte
B.T : texte
B. I : texte
N° FICHE DECLAR INC : texte
L.R : texte
Devis : texte
CODE Imputation : texte
EQUIPE AMENDIS : liste deroulent depuis une plage
Tournnée : texte
Type Maintenance : liste deroulent depuis une plage (TYPE_MAINTENANCE[TYPE MAINTENANCE])
TYPE RESEAU : liste deroulent depuis une plage (TYPE_RESEAU[TYPE RESEAU])
LIEUX DES TRAVAUX : texte
DATE RECUE : date
N° ATTACHEMENT : texte
Date ATTACHEMENT : DATE
---
- 5- les champs de saisi sont tous.
- 6- Les boutons disponibles sont :
- - Exporter les résultats,
- - Supprimer (désactivé par défaut),
- - Nouveau (formulaire vierge),
- - Enregistrer (sauvegarde dans la feuille principale).
- - Fermer (ferme la fenêtre)
- - Recherche (concerne les champs de filtre)
7- exp d'interface:
Merci d’avance pour votre aide, retours, ou partages d’expérience 🙏
Je suis prêt à fournir plus de détails ou partager des extraits de code si besoin !
Salut,
Tu veux un devis ?
Blague à part, on est sur un forum d'entraide, si tu as une demande sur un sujet c'est avec plaisir qu'on va tenter d'apporter une réponse, complète et didactique, mais là tu partages un cahier des charges pour un besoin métier précis, on sort du cadre de l'entraide sur un forum.
Je comprends tout à fait ta remarque. Mon intention n'était pas de poster un cahier des charges en mode "mission freelance", mais plutôt de donner un maximum de contexte pour que ceux qui veulent m’aider ne perdent pas de temps à poser plein de questions de cadrage.
Je suis bien dans une démarche d’entraide, et je suis totalement ouvert à des conseils, des retours d’expérience, ou même des pistes techniques.
Je code tout moi-même, en m’appuyant sur les échanges et l’expérience des membres du groupe. J’ai juste besoin de quelques coups de pouce pour débloquer certaines parties.
Si c’était perçu comme une "demande de prestation", je m’en excuse 🙏
Merci encore à ceux qui prennent le temps de lire et aider !
Ok
Où en es tu dans la conception ? Ta structure à l'air d'être définie.
Quelques conseils en vrac et assez général :
> Si tu comptes manipuler beaucoup de données, je te conseille de travailler avec une vraie BDD et non pas Sheets (à la limite tu peux répliquer sur Sheets au besoins).
> Si tu te sers de Sheets comme BDD, bloque la feuille de BDD, que personne n'y touche ni puisse y toucher.
> Si plusieurs users peuvent intervenir en simultanées, il faut bien prévoir cela pour éviter les conflits.
> Après lecture rapide, je te conseille de passer par de la programmation orientée objet, (un construct New rdv où rdv est ton objet)
> Vu que tes paramètres peuvent changer, prévoit cela pour les rendre dynamiques (par ex un script qui importe les paramètres à chaque lancement).
À noter que je ne suis pas un développeur expérimenté, mais quelqu’un qui s’y connaît un peu et qui sait chercher et rechercher des informations sur Internet pour faciliter ses tâches au travail.
Voici mes codes qui affichent le résultât, mais contient des erreurs d'exécutions et d'affichage.
mes fichiers de Apps Scipt:
- code.gs
- Interventions.html
- Interventions-js.html
- Interventions-style.html
- code.gs:
// @ts-nocheck
/**
* Fonction pour inclure un fichier HTML dans un autre.
* @param {string} filename - Nom du fichier HTML à inclure.
* @return {string} Contenu du fichier HTML.
*/
function include(filename) {
// Utilise le service HtmlService pour créer une sortie HTML à partir du contenu du fichier spécifié.
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/**
* Ajoute un menu personnalisé dans Google Sheets.
* Ce menu permet d'accéder à la gestion des interventions.
*/
function onOpen() {
// Récupère l'interface utilisateur de Google Sheets.
const ui = SpreadsheetApp.getUi();
// Crée un nouveau menu nommé "Gestion des interventions Maintenance BT".
ui.createMenu("Gestion des interventions Maintenance BT")
// Ajoute un élément au menu nommé "Gérer les interventions" qui exécute la fonction "showInterventions" lorsqu'il est cliqué.
.addItem("Gérer les interventions", "showInterventions")
// Ajoute une ligne de séparation dans le menu pour organiser les éléments.
.addSeparator()
// Ajoute le menu créé à l'interface utilisateur de la feuille de calcul.
.addToUi();
}
/**
* Affiche le formulaire dans une fenêtre modale.
* Le formulaire est créé à partir du fichier HTML "Interventions".
*/
function showInterventions() {
// Crée un modèle HTML à partir du fichier "Interventions.html".
const htmlTemplate = HtmlService.createTemplateFromFile("Interventions")
// Évalue le modèle pour générer le contenu HTML.
.evaluate()
// Définit la largeur de la fenêtre modale à une valeur très grande pour occuper la largeur maximale.
.setWidth(9999)
// Définit la hauteur de la fenêtre modale à une valeur très grande pour occuper la hauteur maximale.
.setHeight(9999);
// Affiche une boîte de dialogue modale dans l'interface utilisateur de la feuille de calcul avec le contenu HTML généré et un titre.
SpreadsheetApp.getUi().showModalDialog(htmlTemplate, "Gestion des interventions Maintenance BT");
}
/**
* Récupère toutes les interventions sous forme de JSON.
* @return {string} JSON contenant les interventions ou un message d'erreur.
*/
function getInterventions() {
try {
// Récupère la feuille de calcul nommée "BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)".
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)");
// Si la feuille n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille introuvable.");
// Récupère toutes les données de la plage de données de la feuille sous forme de tableau 2D.
const [headers, ...data] = sheet.getDataRange().getValues();
// Si le tableau de données (sans les en-têtes) est vide, retourne une chaîne JSON représentant un tableau vide.
if (data.length === 0) return JSON.stringify([]);
// Transforme chaque ligne de données en un objet JSON où les clés sont les en-têtes des colonnes.
const interventions = data.map((row) => {
// Crée un objet à partir d'un tableau de paires clé-valeur.
// Les clés sont les en-têtes et les valeurs sont les éléments correspondants de la ligne.
return Object.fromEntries(headers.map((h, i) => [h, row[i]]));
});
// Retourne le tableau d'objets d'interventions sous forme de chaîne JSON.
return JSON.stringify(interventions);
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans getInterventions: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors de la récupération des interventions." });
}
}
/**
* Récupère une intervention par ID.
* @param {number} id - ID de l'intervention.
* @return {string} JSON contenant l'intervention ou un message d'erreur.
*/
function getIntervention(id) {
try {
// Récupère la feuille de calcul nommée "BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)".
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)");
// Si la feuille n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille introuvable.");
// Récupère les en-têtes (première ligne) et toutes les données (à partir de la deuxième ligne).
const [headers, ...data] = sheet.getDataRange().getValues();
// Recherche l'index de la première ligne où la première colonne (supposée contenir l'ID) correspond à l'ID recherché.
const index = data.findIndex((row) => row[0] == id);
// Si aucune intervention avec l'ID spécifié n'est trouvée, retourne une chaîne JSON contenant un message d'erreur.
if (index === -1) return JSON.stringify({ error: "Intervention introuvable." });
// Crée un objet JSON pour l'intervention trouvée en associant les en-têtes aux valeurs de la ligne correspondante.
const intervention = Object.fromEntries(headers.map((h, i) => [h, data[index][i]]));
// Retourne l'objet d'intervention sous forme de chaîne JSON.
return JSON.stringify(intervention);
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans getIntervention: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors de la récupération de l'intervention." });
}
}
/**
* Supprime une intervention par ID.
* @param {number} id - ID de l'intervention.
* @return {string} JSON contenant un message de succès ou d'erreur.
*/
function deleteIntervention(id) {
try {
// Récupère la feuille de calcul nommée "BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)".
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)");
// Si la feuille n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille introuvable.");
// Récupère toutes les données de la feuille.
const data = sheet.getDataRange().getValues();
// Recherche l'index de la première ligne où la première colonne (supposée contenir l'ID) correspond à l'ID recherché.
const index = data.findIndex((row) => row[0] == id);
// Si aucune intervention avec l'ID spécifié n'est trouvée, retourne une chaîne JSON contenant un message d'erreur.
if (index === -1) return JSON.stringify({ error: "Intervention introuvable." });
// Supprime la ligne correspondante à l'index trouvé (l'index est basé sur le tableau de données, donc on ajoute 1 pour correspondre au numéro de ligne dans la feuille).
sheet.deleteRow(index + 1);
// Retourne une chaîne JSON indiquant que la suppression a réussi.
return JSON.stringify({ success: true, message: "Intervention supprimée avec succès." });
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans deleteIntervention: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors de la suppression de l'intervention." });
}
}
/**
* Enregistre ou met à jour une intervention.
* @param {Array} e - Données de l'intervention.
* @return {string} JSON contenant l'ID de l'intervention ou un message d'erreur.
*/
function saveIntervention(e) {
try {
// Récupère la feuille de calcul nommée "BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)".
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)");
// Si la feuille n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille introuvable.");
// Récupère toutes les données de la feuille.
const data = sheet.getDataRange().getValues();
// Récupère l'ID de l'intervention à partir du premier élément du tableau de données 'e'.
let id = e[0];
// Vérifie si un ID existe (indiquant une mise à jour).
if (id) {
// Recherche l'index de la ligne correspondant à l'ID de l'intervention.
const index = data.findIndex((row) => row[0] == id);
// Si l'intervention avec l'ID spécifié n'est pas trouvée, retourne une erreur.
if (index === -1) return JSON.stringify({ error: "Intervention introuvable." });
// Met à jour la ligne correspondante dans la feuille avec les nouvelles données 'e'.
// 'index + 1' car l'index est basé sur le tableau de données (0-based) et getRange utilise des numéros de ligne (1-based).
sheet.getRange(index + 1, 1, 1, e.length).setValues([e]);
// Met à jour la colonne suivant les données avec la date de modification actuelle.
sheet.getRange(index + 1, e.length + 1).setValue(new Date());
// Retourne un message de succès avec l'ID de l'intervention mise à jour.
return JSON.stringify({ success: true, message: "Intervention mise à jour avec succès.", id: id });
} else {
// Création d'une nouvelle intervention (pas d'ID existant).
// Détermine le prochain ID disponible en trouvant le maximum des IDs existants et en ajoutant 1.
// Si la feuille ne contient que les en-têtes, l'ID commence à 1.
id = data.length > 1 ? Math.max(...data.slice(1).map((row) => row[0] || 0)) + 1 : 1;
// Assigne le nouvel ID au premier élément du tableau 'e'.
e[0] = id;
// Ajoute les dates de création et de modification à la fin du tableau 'e'.
e.push(new Date(), new Date());
// Ajoute une nouvelle ligne à la feuille avec les données de la nouvelle intervention.
sheet.appendRow(e);
// Trie les interventions après l'enregistrement de la nouvelle intervention.
sortInterventions();
// Retourne un message de succès avec le nouvel ID de l'intervention créée.
return JSON.stringify({ success: true, message: "Intervention créée avec succès.", id: id });
}
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans saveIntervention: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors de l'enregistrement de l'intervention." });
}
}
/**
* Trie les interventions par ORIGINE, TYPE TRAVAUX et DATE RECUE.
* @return {string} JSON contenant un message de succès ou d'erreur.
*/
function sortInterventions() {
try {
// Récupère la feuille de calcul nommée "BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)".
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)");
// Si la feuille n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille introuvable.");
// Récupère le numéro de la dernière ligne contenant des données.
const lastRow = sheet.getLastRow();
// Vérifie s'il y a des données à trier (plus que la ligne d'en-têtes).
if (lastRow > 1) {
// Trie la plage de données (à partir de la deuxième ligne) en fonction de plusieurs colonnes.
sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).sort([
{ column: 2, ascending: true }, // Trie en premier par la colonne 2 (ORIGINE) en ordre ascendant.
{ column: 3, ascending: true }, // Ensuite, trie par la colonne 3 (TYPE TRAVAUX) en ordre ascendant pour les valeurs identiques dans la colonne 2.
{ column: 25, ascending: true }, // Enfin, trie par la colonne 25 (DATE RECUE) en ordre ascendant pour les valeurs identiques dans les colonnes 2 et 3.
]);
// Retourne un message de succès indiquant que les interventions ont été triées.
return JSON.stringify({ success: true, message: "Interventions triées avec succès." });
} else {
// Retourne un message indiquant qu'il n'y a pas d'interventions à trier.
return JSON.stringify({ success: false, message: "Aucune intervention à trier." });
}
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans sortInterventions: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors du tri des interventions." });
}
}
/**
* Récupère toutes les listes déroulantes de l'onglet "Paramètres".
* @return {string} JSON contenant les listes déroulantes ou un message d'erreur.
*/
function getDropdownLists() {
try {
// Récupère la feuille de calcul nommée "Paramètres".
const sheet = getSheetByName("Paramètres");
// Si la feuille "Paramètres" n'est pas trouvée, lance une erreur.
if (!sheet) throw new Error("Feuille 'Paramètres' introuvable.");
// Récupère les valeurs des plages nommées correspondant aux listes déroulantes.
// .getValues() retourne un tableau 2D, .flat() le transforme en tableau 1D, et .filter(String) élimine les chaînes vides.
return JSON.stringify({
origine: sheet.getRangeByName("Origine").getValues().flat().filter(String),
typeTravaux: sheet.getRangeByName("TYPE_TRAVAUX").getValues().flat().filter(String),
typeMedia: sheet.getRangeByName("TYPE_MEDIA").getValues().flat().filter(String), // Conserve la récupération de TYPE_MEDIA
equipeAmendis: sheet.getRangeByName("LES_EQUIPES").getValues().flat().filter(String),
typeMaintenance: sheet.getRangeByName("TYPE_MAINTENANCE").getValues().flat().filter(String),
typeReseau: sheet.getRangeByName("TYPE_RESEAU").getValues().flat().filter(String),
listeObjet: sheet.getRangeByName("LISTE_OBJET").getValues().flat().filter(String),
typeTension: sheet.getRangeByName("TYPE_TR_TENSION").getValues().flat().filter(String),
equipePrestataire: sheet.getRangeByName("Equipe_Prestataire").getValues().flat().filter(String),
agentJuridique: sheet.getRangeByName("AGENT_JURIDIQUE").getValues().flat().filter(String),
});
} catch (error) {
// Enregistre l'erreur dans les journaux de Google Apps Script.
Logger.log("Erreur dans getDropdownLists: " + error.message);
// Retourne une chaîne JSON contenant un message d'erreur générique.
return JSON.stringify({ error: "Une erreur s'est produite lors de la récupération des listes déroulantes." });
}
}
/**
* Récupère une feuille par son nom.
* @param {string} sheetName - Nom de la feuille.
* @return {GoogleAppsScript.Spreadsheet.Sheet} La feuille correspondante ou null si elle n'existe pas.
*/
function getSheetByName(sheetName) {
// Récupère la feuille de calcul active.
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// Retourne la feuille de calcul avec le nom spécifié, ou null si elle n'est pas trouvée.
return spreadsheet.getSheetByName(sheetName);
}- Interventions.html
<!DOCTYPE html>
<html>
<head>
<!-- Inclusion du fichier de styles CSS "Interventions-style" via Google Apps Script -->
<?!= include('Interventions-style'); ?>
</head>
<body>
<!-- Section de recherche des Interventions -->
<div id="search">
<!-- Champ de recherche pour l'ID (Numéro Automatique) -->
<label for="search0">ID (Numéro Automatique)</label>
<select id="search0"></select>
<!-- Champ de recherche pour l'ORIGINE -->
<label for="search1">ORIGINE</label>
<select id="search1"></select>
<!-- Champ de recherche pour le Type Travaux -->
<label for="search2">Type Travaux</label>
<select id="search2"></select>
<!-- Case à cocher pour Urgent -->
<label for="search3">Urgent</label>
<input type="checkbox" id="search3" value="">
<!-- Case à cocher pour Demande Terrassement -->
<label for="search4">Demande Terrassement</label>
<input type="checkbox" id="search4" value="">
<!-- Champ de texte pour Bon Sortie 1T45XZT0096 -->
<label for="search5">Bon Sortie 1T45XZT0096</label>
<input type="text" id="search5" value="" autocomplete="new-password">
<!-- Champ de texte pour Bon Retour 1T45XZT0096 -->
<label for="search6">Bon Retour 1T45XZT0096</label>
<input type="text" id="search6" value="" autocomplete="new-password">
<!-- Champ de texte pour Bon Sortie -->
<label for="search7">Bon Sortie</label>
<input type="text" id="search7" value="" autocomplete="new-password">
<!-- Champ de texte pour Bon Retour -->
<label for="search8">Bon Retour</label>
<input type="text" id="search8" value="" autocomplete="new-password">
<!-- Champ de texte pour B.T -->
<label for="search9">B.T</label>
<input type="text" id="search9" value="" autocomplete="new-password">
<!-- Champ de texte pour B.I -->
<label for="search10">B.I</label>
<input type="text" id="search10" value="" autocomplete="new-password">
<!-- Champ de texte pour N° FICHE DECLAR INC -->
<label for="search11">N° FICHE DECLAR INC</label>
<input type="text" id="search11" value="" autocomplete="new-password">
<!-- Champ de texte pour L.R -->
<label for="search12">L.R</label>
<input type="text" id="search12" value="" autocomplete="new-password">
<!-- Champ de texte pour Devis -->
<label for="search13">Devis</label>
<input type="text" id="search13" value="" autocomplete="new-password">
<!-- Champ de texte pour CODE Imputation -->
<label for="search14">CODE Imputation</label>
<input type="text" id="search14" value="" autocomplete="new-password">
<!-- Champ de liste déroulante pour EQUIPE AMENDIS -->
<label for="search15">EQUIPE AMENDIS</label>
<select id="search15"></select>
<!-- Champ de texte pour Tournée -->
<label for="search16">Tournée</label>
<input type="text" id="search16" value="" autocomplete="new-password">
<!-- Champ de liste déroulante pour Type Maintenance -->
<label for="search17">Type Maintenance</label>
<select id="search17"></select>
<!-- Champ de liste déroulante pour TYPE RESEAU -->
<label for="search18">TYPE RESEAU</label>
<select id="search18"></select>
<!-- Champ de texte pour LIEUX DES TRAVAUX -->
<label for="search19">LIEUX DES TRAVAUX</label>
<input type="text" id="search19" value="" autocomplete="new-password">
<!-- Champ de date pour DATE RECUE -->
<label for="search20">DATE RECUE</label>
<input type="date" id="search20" value="" autocomplete="new-password">
<!-- Champ de texte pour N° ATTACHEMENT -->
<label for="search21">N° ATTACHEMENT</label>
<input type="text" id="search21" value="" autocomplete="new-password">
<!-- Champ de date pour Date ATTACHEMENT -->
<label for="search22">Date ATTACHEMENT</label>
<input type="date" id="search22" value="" autocomplete="new-password">
</div>
<!-- Liste des Interventions affichées en fonction de la recherche -->
<div id="list">
<!-- Icône ou animation de chargement en SVG -->
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iIzA1YjM4ZSIgZD0iTTEyLDRWMkExMCwxMCAwIDAsMCAyLDEySDRBOCw4IDAgMCwxIDEyLDRaIi8+PC9zdmc+">
</div>
<!-- Affichage du nombre total d'Interventions trouvés -->
<div id="total">...</div>
<!-- Formulaire pour afficher ou modifier une intervention -->
<div id="form"> </div>
<!-- Boutons d'actions pour gérer les Interventions -->
<div id="buttons">
<!-- Bouton pour fermer la fenêtre -->
<input type="button" id="close" value="Fermer">
<!-- Bouton pour exporter les résultats -->
<input type="button" id="export" value="Exporter les résultats" title="Exporter les résultats dans une nouvelle feuille du classeur">
<!-- Bouton de suppression (désactivé par défaut) -->
<input type="button" id="delete" value="Supprimer" disabled>
<!-- Bouton pour créer une nouvelle intervention -->
<input type="button" id="new" value="Nouveau">
<!-- Bouton pour sauvegarder une intervention -->
<input type="button" id="save" value="Enregistrer">
</div>
<!-- Inclusion du fichier JavaScript "Interventions-js" via Google Apps Script -->
<?!= include('Interventions-js'); ?>
</body>
</html>- Interventions-js.html
<script>
/*
* Ce script gère la liste des interventions avec des fonctionnalités de recherche, affichage, sélection, modification et suppression.
*/
/* Déclaration des variables globales */
let interventions, list; // interventions : stocke toutes les interventions ; list : stocke les interventions filtrées
/* Définition des indices des colonnes utilisées dans Google Sheets.
COL_ID correspond aux numéros des colonnes où les informations des interventions sont stockées. */
const COL_ID = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 21, 22, 39];
/* Récupération des champs de recherche dans le formulaire HTML.
Ces champs permettent à l'utilisateur de rechercher parmi les interventions. */
const SEARCH_INPUTS = [
document.getElementById("search0"), // ID
document.getElementById("search1"), // ORIGINE
document.getElementById("search2"), // Type Travaux
document.getElementById("search3"), // Urgent
document.getElementById("search4"), // Demande Terrassement
document.getElementById("search5"), // Bon Sortie 1T45XZT0096
document.getElementById("search6"), // Bon Retour 1T45XZT0096
document.getElementById("search7"), // Bon Sortie
document.getElementById("search8"), // Bon Retour
document.getElementById("search9"), // B.T
document.getElementById("search10"), // B. I
document.getElementById("search11"), // N° FICHE DECLAR INC
document.getElementById("search12"), // L.R
document.getElementById("search13"), // Devis
document.getElementById("search14"), // CODE Imputation
document.getElementById("search15"), // EQUIPE AMENDIS
document.getElementById("search16"), // Tournée
document.getElementById("search17"), // Type Maintenance
document.getElementById("search18") // TYPE RESEAU
];
/**
* Récupérer les interventions depuis Google Sheets via Google Apps Script.
* Cette fonction appelle le service Google Apps Script pour récupérer les interventions depuis une feuille Google Sheets.
*/
function getInterventions() {
const isFirstLoad = interventions === undefined; // Vérifie si c'est le premier chargement des interventions
if (!isFirstLoad) refreshInterventions(); // Si ce n'est pas la première fois, rafraîchir la liste des interventions
// Exécuter la fonction Apps Script "getInterventions" et rafraîchir la liste des interventions
google.script.run.withSuccessHandler((data) => {
refreshInterventions(data, document.querySelector("div.selection")?.dataset.id, isFirstLoad);
}).withFailureHandler((error) => {
console.error("Erreur lors de la récupération des interventions :", error); // Gestion des erreurs
alert("Une erreur s'est produite lors de la récupération des interventions. Veuillez réessayer."); // Alerte utilisateur
}).getInterventions();
}
/**
* Rafraîchir la liste des interventions après récupération ou recherche.
* Cette fonction met à jour la liste des interventions affichées en fonction des critères de recherche.
* @param {string} data - Données JSON des interventions
* @param {string} selectedId - ID de l'intervention actuellement sélectionnée (si applicable)
* @param {boolean} isFirstLoad - Indique si c'est le premier chargement
*/
function refreshInterventions(data, selectedId, isFirstLoad = false) {
if (data != null) interventions = JSON.parse(data); // Convertit les données JSON en tableau JavaScript
if (!isFirstLoad) reset(); // Réinitialise la sélection si ce n'est pas la première récupération
// Récupérer les valeurs saisies dans les champs de recherche
const searchValues = SEARCH_INPUTS.map(input => input.value);
// Transformer chaque champ de recherche en expression régulière (regex) pour filtrer les interventions
const regexFilters = searchValues.map(value => new RegExp(escapeRegex(value), "i"));
list = []; // Réinitialisation de la liste des interventions trouvées après filtrage
// Appliquer le filtre et sélectionner uniquement les interventions qui correspondent aux critères de recherche
interventions.forEach(intervention => {
if (
(searchValues[0] === "" || intervention[COL_ID[0]] == searchValues[0]) && // ID
(searchValues[1] === "" || regexFilters[1].test(intervention[COL_ID[1]])) && // ORIGINE
(searchValues[2] === "" || regexFilters[2].test(intervention[COL_ID[2]])) && // Type Travaux
(searchValues[3] === "" || regexFilters[3].test(intervention[COL_ID[3]])) && // Urgent
(searchValues[4] === "" || regexFilters[4].test(intervention[COL_ID[4]])) && // Demande Terrassement
(searchValues[5] === "" || regexFilters[5].test(intervention[COL_ID[5]])) && // Bon Sortie 1T45XZT0096
(searchValues[6] === "" || regexFilters[6].test(intervention[COL_ID[6]])) && // Bon Retour 1T45XZT0096
(searchValues[7] === "" || regexFilters[7].test(intervention[COL_ID[7]])) && // Bon Sortie
(searchValues[8] === "" || regexFilters[8].test(intervention[COL_ID[8]])) && // Bon Retour
(searchValues[9] === "" || regexFilters[9].test(intervention[COL_ID[9]])) && // B.T
(searchValues[10] === "" || regexFilters[10].test(intervention[COL_ID[10]])) && // B. I
(searchValues[11] === "" || regexFilters[11].test(intervention[COL_ID[11]])) && // N° FICHE DECLAR INC
(searchValues[12] === "" || regexFilters[12].test(intervention[COL_ID[12]])) && // L.R
(searchValues[13] === "" || regexFilters[13].test(intervention[COL_ID[13]])) && // Devis
(searchValues[14] === "" || regexFilters[14].test(intervention[COL_ID[14]])) && // CODE Imputation
(searchValues[15] === "" || regexFilters[15].test(intervention[COL_ID[15]])) && // EQUIPE AMENDIS
(searchValues[16] === "" || regexFilters[16].test(intervention[COL_ID[16]])) && // Tournée
(searchValues[17] === "" || regexFilters[17].test(intervention[COL_ID[17]])) && // Type Maintenance
(searchValues[18] === "" || regexFilters[18].test(intervention[COL_ID[18]])) && // TYPE RESEAU
) {
list.push([
intervention[0],
intervention[COL_ID[0]],
intervention[COL_ID[1]],
intervention[COL_ID[2]],
intervention[COL_ID[3]],
intervention[COL_ID[4]],
intervention[COL_ID[5]],
intervention[COL_ID[6]],
intervention[COL_ID[7]],
intervention[COL_ID[8]],
intervention[COL_ID[9]],
intervention[COL_ID[10]],
intervention[COL_ID[11]],
intervention[COL_ID[12]],
intervention[COL_ID[13]],
intervention[COL_ID[14]],
intervention[COL_ID[15]],
intervention[COL_ID[16]],
intervention[COL_ID[17]],
intervention[COL_ID[18]],
]);
}
});
// Met à jour l'affichage de la liste des interventions
document.getElementById("list").innerHTML = list.map(intervention =>
`<div data-id="${intervention[0]}" role="button" aria-pressed="false" tabindex="0"><div>${intervention.slice(1).join("</div><div>")}</div></div>`
).join("");
// Ajouter un événement de sélection à chaque intervention affichée
document.querySelectorAll("#list > div").forEach(element =>
element.addEventListener("click", () => selectIntervention(element.dataset.id))
);
// Ajouter un événement de clavier pour la sélection avec la touche "Entrée"
document.querySelectorAll("#list > div").forEach(element =>
element.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
selectIntervention(element.dataset.id);
}
})
);
// Met à jour le nombre total d'interventions affichées
nbInterventions();
// Désactiver le bouton d'export si aucune intervention n'est trouvée
document.getElementById("export").disabled = !list.length;
// Si une intervention était sélectionnée, la resélectionner après la mise à jour
if (selectedId) selectIntervention(selectedId, true);
}
/**
* Met à jour le nombre d'interventions trouvées et l'affiche.
* Cette fonction met à jour l'affichage du nombre d'interventions trouvées après chaque filtrage.
*/
function nbInterventions() {
const count = list.length; // Compte le nombre d'interventions trouvées
document.getElementById("total").textContent = `${count} intervention${count > 1 ? "s" : ""} trouvé${count > 1 ? "s" : ""}`; // Met à jour le texte affiché
}
/**
* Sélectionner une intervention et afficher ses informations dans le formulaire.
* Cette fonction met à jour l'interface utilisateur pour afficher les informations de l'intervention sélectionnée.
* @param {string} id - L'ID de l'intervention à sélectionner
* @param {boolean} isReselect - Indique si l'intervention est resélectionnée
*/
function selectIntervention(id, isReselect = false) {
const selectedIntervention = list.find(intervention => intervention[0] === id); // Trouver l'intervention sélectionnée
if (!selectedIntervention) return; // Si aucune intervention n'est trouvée, sortir de la fonction
// Mettre à jour les champs du formulaire avec les informations de l'intervention sélectionnée
document.getElementById("id").value = selectedIntervention[1];
document.getElementById("origine").value = selectedIntervention[2];
document.getElementById("typeTravaux").value = selectedIntervention[3];
document.getElementById("urgent").checked = selectedIntervention[4] === "Oui";
document.getElementById("demandeTerrassement").value = selectedIntervention[5];
document.getElementById("bonSortie").value = selectedIntervention[6];
document.getElementById("bonRetour").value = selectedIntervention[7];
document.getElementById("bt").value = selectedIntervention[8];
document.getElementById("bi").value = selectedIntervention[9];
document.getElementById("ficheDeclarInc").value = selectedIntervention[10];
document.getElementById("lr").value = selectedIntervention[11];
document.getElementById("devis").value = selectedIntervention[12];
document.getElementById("codeImputation").value = selectedIntervention[13];
document.getElementById("typeMedia").value = selectedIntervention[14];
document.getElementById("equipeAmendis").value = selectedIntervention[15];
document.getElementById("tournee").value = selectedIntervention[16];
document.getElementById("typeMaintenance").value = selectedIntervention[17];
document.getElementById("typeReseau").value = selectedIntervention[18];
// Met à jour l'état de l'élément sélectionné dans la liste
document.querySelectorAll("#list > div").forEach(element => {
element.setAttribute("aria-pressed", element.dataset.id === id ? "true" : "false"); // Met à jour l'attribut ARIA
});
// Si l'intervention est resélectionnée, ne pas réinitialiser le formulaire
if (!isReselect) reset(); // Réinitialiser le formulaire si ce n'est pas une resélection
}
/**
* Réinitialiser le formulaire de sélection d'intervention.
* Cette fonction remet à zéro tous les champs du formulaire pour permettre une nouvelle sélection.
*/
function reset() {
document.getElementById("id").value = "";
document.getElementById("origine").value = "";
document.getElementById("typeTravaux").value = "";
document.getElementById("urgent").checked = false;
document.getElementById("demandeTerrassement").value = "";
document.getElementById("bonSortie").value = "";
document.getElementById("bonRetour").value = "";
document.getElementById("bt").value = "";
document.getElementById("bi").value = "";
document.getElementById("ficheDeclarInc").value = "";
document.getElementById("lr").value = "";
document.getElementById("devis").value = "";
document.getElementById("codeImputation").value = "";
document.getElementById("typeMedia").value = "";
document.getElementById("equipeAmendis").value = "";
document.getElementById("tournee").value = "";
document.getElementById("typeMaintenance").value = "";
document.getElementById("typeReseau").value = "";
}
/**
* Fonction pour échapper les caractères spéciaux dans les expressions régulières.
* @param {string} string - La chaîne à échapper
* @return {string} La chaîne échappée
*/
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Échappe les caractères spéciaux
}
// Appel initial pour récupérer les interventions
getInterventions();
</script>
<!-- Ajout d'attributs ARIA pour améliorer l'accessibilité -->
<div id="list" role="list" aria-label="Liste des interventions" tabindex="0"></div>
<button id="export" aria-disabled="true" onclick="exportInterventions()">Exporter</button>
<div id="total" role="status" aria-live="polite"></div>
<script>
/**
* Fonction pour exporter les interventions.
* Cette fonction permet d'exporter les interventions filtrées.
*/
function exportInterventions() {
try {
// Logique d'exportation des interventions
if (list.length === 0) {
alert("Aucune intervention à exporter.");
return;
}
// Code pour exporter les données
console.log("Exportation des interventions:", list);
alert("Exportation réussie !");
} catch (error) {
console.error("Erreur lors de l'exportation des interventions :", error);
alert("Une erreur s'est produite lors de l'exportation. Veuillez réessayer.");
}
}
</script>- Interventions-style.html
<style>
/* Définir des variables CSS pour les couleurs et les tailles */
:root {
--font-size-base: 0.94rem;
--color-primary: #30a392;
--color-secondary: #f3f3f3;
--color-text: #333;
--color-text-light: #fff;
--color-hover: #40ad96;
--color-disabled: silver;
--color-selection: #05b38e;
}
/* Définir la taille de la police pour le document */
html {
font-size: var(--font-size-base);
}
/* Mise en page principale du body, utilisant une grille pour organiser les éléments */
body {
display: grid;
grid-template-columns: 2fr;
grid-template-rows: minmax(min-content, max-content) minmax(20%, 2fr) minmax(min-content, max-content) 2fr minmax(min-content, max-content);
height: 100vh;
margin: 0;
color: var(--color-text);
font-family: Roboto, Arial, sans-serif;
overflow: hidden;
}
/* Style pour la section de recherche */
#search {
display: grid;
grid-template-columns: repeat(8, minmax(0, 2fr));
grid-gap: .5rem;
padding: 1rem;
box-sizing: border-box;
background: var(--color-primary);
border-radius: .4rem .4rem 0 0;
}
/* Style pour les éléments à l'intérieur de la section de recherche */
#search > div {
margin-top: -.4rem;
text-align: center;
color: var(--color-text-light);
}
/* Style pour la liste des interventions */
#list {
display: grid;
grid-template-columns: 2fr;
grid-auto-rows: minmax(min-content, max-content);
width: 100%;
box-sizing: border-box;
background: var(--color-secondary);
border-top: 1px solid var(--color-secondary);
border-radius: 0 0 .4rem .4rem;
overflow-y: auto;
}
/* Style pour chaque intervention dans la liste */
#list > div {
display: grid;
grid-template-columns: repeat(8, minmax(0, 2fr));
grid-gap: .5rem;
padding: .3rem 1rem .2rem;
cursor: pointer;
}
/* Style pour les interventions à lignes paires */
#list > div:nth-child(even) {
background: #f9f9f9;
}
/* Effet de survol pour les interventions dans la liste */
#list > div:hover,
#list > div:nth-child(even):hover {
background: #e5e5e5;
color: #222;
}
/* Style pour une intervention sélectionnée */
div.selection {
background: var(--color-selection) !important;
color: var(--color-text-light) !important;
}
/* Style pour chaque division dans une intervention */
#list > div > div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* Animation pour les images (rotation) */
#list > img {
display: block;
margin: 15vh auto 0;
width: 40px;
animation: s .7s infinite linear;
}
/* Animation de rotation infinie */
@keyframes s {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Style pour la section des informations de l'intervention */
#info {
display: grid;
grid-template-columns: minmax(min-content, max-content) 2fr;
padding: .5rem 0;
}
/* Style pour les éléments dans la section des informations */
#info div, #info a {
font-size: .94rem;
color: #999;
text-decoration: none;
}
/* Effet de survol sur les liens dans les informations */
#info a:hover {
text-decoration: underline;
}
/* Aligner le total des interventions à droite */
#total {
text-align: right;
}
/* Style du formulaire */
#form {
display: grid;
grid-template-columns: 2fr 2fr 2fr;
grid-gap: 1rem;
padding: 1rem;
background: var(--color-secondary);
border-radius: .4rem;
}
/* Style pour chaque champ du formulaire */
#form > div {
display: grid;
grid-template-columns: 2fr 2fr;
grid-auto-rows: minmax(calc((100% - 2.2rem) / 12), max-content);
grid-gap: .2rem;
}
/* Style pour les zones de texte (textarea) */
#form textarea {
grid-column: 1 / 3;
grid-row: 7 / 13;
}
/* Style pour la section des boutons du formulaire */
#buttons {
display: grid;
grid-template-columns: 2fr 1.5fr .5fr 2fr 2fr 2fr;
grid-gap: .5rem;
margin-top: 1rem;
}
/* Style pour les paragraphes dans le formulaire */
p {
margin: 0;
line-height: 2.1rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* Style pour les champs de texte, sélecteurs, et textarea */
input[type="text"], select, textarea {
display: inline-block;
box-sizing: border-box;
min-width: 3rem;
padding: .4rem 0 .35rem .5rem;
font-family: Roboto, Arial, sans-serif;
color: #444;
background: #fff;
border: none;
font-size: 1rem;
border-radius: .4rem;
}
/* Style pour les boutons */
input[type="button"] {
display: inline-block;
padding: .7rem 0 .6rem;
font-family: Roboto, Arial, sans-serif;
border: none;
background: var(--color-primary);
color: var(--color-text-light);
font-size: 1.15rem;
cursor: pointer;
border-radius: .4rem;
transition: background 0.3s; /* Ajout de transition */
}
/* Effet de survol sur les boutons */
input[type="button"]:hover {
background: var(--color-hover);
}
/* Style pour les boutons désactivés */
input[type="button"]:disabled {
background: var(--color-disabled);
cursor: default;
}
/* Style pour les champs désactivés */
input:disabled {
width: 100%;
background: #ccc;
}
/* Style pour les icônes SVG */
svg {
height: 1.6rem;
fill: var(--color-text-light);
position: absolute;
margin-left: -1.9rem;
margin-top: .18rem;
}
/* Style pour les barres de défilement */
body::-webkit-scrollbar, #list::-webkit-scrollbar, textarea::-webkit-scrollbar {
width: 10px;
height: 10px;
-webkit-appearance: none;
background: rgba(0, 0, 0, .1);
}
body::-webkit-scrollbar-thumb, #list::-webkit-scrollbar-thumb, textarea::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, .15);
}
/* Styles réactifs pour les écrans plus petits */
@media(max-width: 1000px) {
html { font-size: .88rem; }
}
@media(max-height: 800px) {
p { line-height: 1.9rem; }
}
@media(max-height: 750px) {
html { font-size: .88rem; }
#search { padding: 1rem .5rem .5rem; }
#list > div { padding: .3rem .5rem .2rem; }
#form { padding: .5rem; }
}
@media(max-width: 700px) {
html { font-size: .84rem; }
}
@media(max-height: 650px) {
html { font-size: .8rem; }
#search { padding: .7rem .4rem .5rem; grid-gap: .3rem; }
#list > div { padding: . 2rem .5rem .19rem; }
#form { padding: .4rem; }
#info { padding: .3rem 0; }
#buttons { margin-top: .5rem; }
input[type="button"] { padding: .5rem 0 .48rem; font-size: 1.1rem; }
input[type="text"], select, textarea { padding: .3rem 0 .25rem .5rem; }
}
@media(max-height: 550px) {
body { overflow-y: auto; }
}
</style>voici ma fichier google sheet;
Gestion des interventions Maintenance BT-(SUIVI B.T. & L.R & DEVIS) - Version teste
Merci et DSL mes amies
Voici quelques points après une lecture en diagonale :
GAS
tu peux alléger/simplifier en mettant des constantes/variables dans la portée globale au lieu de les déclarer à chaque fois par exemple :
const sheet = getSheetByName("BD_Maintenance_SUIVI_(B.T./L.R./DEVIS)"); est déclaré 5 fois, met le en global genre "SHEET_BD" (tu peux aussi mettre des données Date ou autre qui sont réutilisées en global pour ne pas avoir à rappeler plusieurs fois les mêmes données).
Idem, quand tu recherche à travailler sur une intervention, créer une fonction : input = ID / retrun obj intervention ou bien l'index de cette ci pour ne pas répéter x fois la même logique.
HTML + JS
Pourquoi autocomplete="new-password" à de multiples éléments ?
interventions, list > il serait plus propre, simple à maintenir et évolutif de passer par de la POO et un constructor.
L'intégration des données pourrait être simplifiée, avec dataTable ou bootstrap par exemple.
Je metterai les ID des colonnes dans la feuille paramètre de ton fichier, plutôt que de les déclarer dans COL_ID car en cas de modification c'est plus complexe à maintenir.
Il peut être judicieux de remplacer reset() qui agit sur une liste définie d’éléments par une fonction qui itère sur chaque type d'input, ainsi, si demain tu ajoutes / modifies / supprime un input, ce sera dynamique.
je n'ai pas regardé le .css car ce n'est que du cosmétique.
Bonjours mes amis,
J'ai fait quelques modifications au code, mais je n'ai pas pu le terminer à cause de ma maladie (40 jours) qui se manifeste par des douleurs lombaires. Une fois rétabli, nos nous retrouverons. Je vous demande de prier pour moi