Logeas Informatique est une société coopérative et participative (SCOP) spécialisée sur le marché du conseil en systèmes et des logiciels informatiques, à destination des associations et des petites et moyennes entreprises.
Logeas Informatique commercialise une solution de gestion d’organisation. Le logiciel LoGeAs, facile d’utilisation, permet la prise en compte des exigences légales et réglementaires (comptabilité, gestion des fichiers, reçus fiscaux, etc.).
LoGeAs est un logiciel sur mesure développé à partir de la plateforme RAD Delphi.
LoGeAs est en cours de réécriture afin de passer en FullWeb. Pour cette nouvelle version du logiciel, nous souhaitons ajouter une petit API liée à notre base de connaissance afin que nos utilisateurs puissent avoir des réponses sans passer par l'assistance (mail/téléphone).
Avoir une API intégrer à la future version de LoGeAs.
Avoir deux interface : Utilisateurs et Assistance.
Avoir une base de connaissance référençant les demandes (et réponses) récurrentes reçues à l'assistance.
Donner un accès plus facile à la documentation au travers de :
(Dans un deuxième temps permettre de contacter l’assistance si la base de connaissance ne propose pas une réponse satisfaisante)
L'utilisateur pourra poser des questions (ex : “Comment éditer un reçu fiscal ?”) et obtenir un choix de réponses liées à sa question. Cela lui fera gagner du temps et le nombre de demandes récurrentes envoyées à l'assistance sera diminué.
L'équipe chargée de l'assistance pourra ajouter des réponses à notre base de connaissance via cette API. Elle pourra également modifier les réponses existantes en cas de besoin. Y ajouter des mot clés etc…
Cette API sera disponible à tous les utilisateurs de notre application initiale.
La création de cette API est réalisée par :
Collaborateurs | Fonction | Niveau en Dev |
---|---|---|
Nicolas MARCHAND | Chef de projet / co-gérant de Logeas Informatique | +++++++ |
Alexia LABARTHE | Apprentie Dev, anciennement en CDI à l'assistance du logiciel LoGeAs | Débutante en apprentissage |
Mathys PAQUEREAU | Apprenti Dev | Débutant en apprentissage |
Fonctions et procédures que l'on a développé
Type | Fonctionnalité | Utilité | Accessible par | Niveau |
---|---|---|---|---|
Fonction | MotsToListeID | Converti une question (liste de mot) en la liste de leurs Ids Rend la liste des ID des mots de la question | X | Back |
Fonction | GetMotClefReponse | Récupère la liste des MotClef liés à l'ID de la Réponse sélectionnée Rend la liste des ID des MotClefs | X | Back |
Fonction | GetJsonFromSQL | Rend le résultat de la requête SQL au format JSON | X | Back |
Fonction | NotifyBeforeURI | X | Back | |
Fonction | AddOrUpdate | X | Back | |
Fonction | Ajoute | X | Back | |
Fonction | AjouteMotVide | X | Back | |
Fonction | GetNonoModel | X | Back | |
Fonction | MUppercase | X | Back | |
Fonction | AjouteDoc | X | Back | |
Constructor | Create | X | ||
Destructor | Destroy | X | ||
Procédure | Logeas | X | ||
Procédure | Recherche | Récupère la Question posée par l'utilisateur Rend la liste des réponses liées aux MotsClefs de la Question posée par l'utilisateur | Tout le monde | Back/Front |
Procédure | EnvoiEmail | Tout le monde | Back | |
Procédure | IncrementReponse | Ajoute +1 au champ NbUse dans la table MotClef | X | Back |
Procédure | GetRep | X | Back/Front | |
Procédure | SaveRep | Récupère les éléments modifiés d'une réponse et/ou d'un/des éléments Rend un JSON comprenant les éléments modifiés | Assistance | Back/Front |
Procédure | SaveRepBig | Identique à SaveRep, mais prends en compte le volume des éléments Récupère les éléments modifiés d'une réponse et/ou d'un/des éléments Rend un JSON comprenant les éléments modifiés | Assistance | Back/Front |
Procédure | SetMotClef | Converti une saisie (liste de mot) en une liste de leurs Ids Rend la liste des ID des MotClefs | Assistance | Back/Front |
Procédure | GetMotClef | Récupère la liste des MotClef liés à l'ID de la Réponse sélectionnée Rend la liste des ID des MotClefs | Tout le monde | Back/Front |
Le back sera réalisé en Free Pascal Lazarus, complété de l’ORM mORMot déjà utilisé par LoGeAs Web
Lazarus est un éditeur de code multiplateforme compatible Delphi pour le développement rapide d'applications.
Lazarus utilise Free Pascal comme langage qui est un dialecte Object Pascal. Il est constamment développé pour intégrer de nouvelles fonctionnalités que l'on peut attendre des langages de programmation modernes.
L’ORM mORMot est un Open SourceServeur clientORMSOAFramework MVC pour Delphi 6 jusqu'à la dernière version Delphi disponible. Les principales caractéristiques de mORMot sont donc :
Avec un accès local ou distant, via un Client-Serveur auto-configurableConception REPOS.
Le front sera réalisé en Angular (version 15) avec la bibliothèque DevExtreme
Angular est un framework open source JavaScript développé par Google. Il est utilisé pour développer des applications web et mobile. Avec cette technologie, on réalise des interfaces de type monopage ou “one page” qui fonctionnent sans rechargement de la page web.
DevExtreme est une bibliothèque de plus de 70 composants réactifs et tactiles pour les applications Angular.
La suite DevExtreme comprend une grille de données, des graphiques interactifs, des éditeurs de données, des composants de navigation et d'interface utilisateur polyvalents.
La base de donnée sera sous SQLite
SQLite est un système de gestion de base de données relationnelle, il regroupe en un seul fichier toutes les tables stockées dans la base de données.
Les documents seront au format HTML stocké dans la base de donnée
La documentation se fait par Compodoc et sur notre WiKi Logeas
Compodoc est un outil open source de documentation pour les applications Angular. Il génère une documentation statique de notre application.
Schématisation de la base de donnée avec le logiciel looping en libre accès et totalement gratuit : Looping
Notre base SQLite a été créée en utilisant l'ORM mORMot. Par simplification je n'ai pas fait apparaitre le champ ID de chaque table dans le MLD, celui-ci étant inclus par la classe de base de toute les tables générées par cet ORM.
{ TSQLReponse } TTypeReponse = (ttr_Assistance,ttr_Tous,ttr_Compte,ttr_ModelEcriture); TSQLReponse = class(TSQLRecord) private fIDTypeBase: TID; fNbUse: Integer; fText: RawUTF8; fTitre: RawUTF8; fTypeReponse: TTypeReponse; published property Titre: RawUTF8 read fTitre write fTitre; property Text: RawUTF8 read fText write fText; property IDTypeBase:TID read fIDTypeBase write fIDTypeBase; property NbUse: Integer read fNbUse write fNbUse; property TypeReponse: TTypeReponse read fTypeReponse write fTypeReponse; public class function AddOrUpdate(Rest:TSQLRest; var Json:RawUTF8):Integer; {$IFDEF LOGEAS_WEB}class function AjouteDoc(Rest:TSQLRest; Titre,Text,Createur:RawUTF8; IdTypeBase:TID):TID;{$ENDIF} end;
{ TSQLMotClef } TSQLMotClef = class(TSQLRecord) private fIDGroupe: TID; fMot: RawUTF8; fNbUse: Integer; fSansSens: Boolean; fSoundex: RawUTF8; published property IDGroupe:TID read fIDGroupe write fIDGroupe; property Mot: RawUTF8 read fMot write fMot; property Soundex: RawUTF8 read fSoundex write fSoundex; property NbUse: Integer read fNbUse write fNbUse; property SansSens:Boolean read fSansSens write fSansSens; public class function Ajoute(Rest:TSQLRest; aMot:String; IsMotVide:Boolean=False):TID; {$IFNDEF FPC}class procedure AjouteMotVide(Rest:TSQLRest); {$ENDIF} class function MotsToListeID(Rest:TSQLRest; Chaine:String; AjouteIDSiInconnu:Boolean=False; AjouteSurServeur:Boolean=True): String; class function GetMotClefReponse(Rest: TSQLRest; IDRep:TID):String; end;
{ TSQLLien } TSQLLien = class(TSQLRecord) private fIDMotClef: TSQLMotClef; fIDReponse: TSQLReponse; published property IDReponse:TSQLReponse read fIDReponse write fIDReponse; property IDMotClef: TSQLMotClef read fIDMotClef write fIDMotClef; public class function Ajoute(Rest:TSQLRest; IDRep,IDMot:TID):TID; end;
Regardons plus en détail la procédure Recherche
Déclaration de notre procédure avec un exemple
L’exemple nous indique quel URL on va devoir envoyer au serveur pour utiliser cette fonctionnalité.
//procedure de recherche Question -> Reponses procedure Recherche(Ctxt: TSQLRestServerURIContext); //http://localhost:8087/root/Recherche?IdTypeBase=0&Question=Comment%20imputer%20les%20salaires%3F&TypeReponse=1
Mise en place de la procédure Recherche
procedure TNonoServer.Recherche(Ctxt: TSQLRestServerURIContext); Var Question:RawUTF8; IdTypeBase:Integer; TypeReponse:TTypeReponse; ListeIDMot,ListeIDSoundex:String; Res:String; SQL,SQLWhere:RawUTF8; begin Question:=Ctxt.InputUTF8['Question']; IdTypeBase:=Ctxt.InputInt['IdTypeBase']; TypeReponse:=TTypeReponse(Ctxt.InputInt['TypeReponse']); if length(question)<2 then begin Ctxt.Returns('{}'); exit; end; if Question[1]='=' then begin Question := Trim(Copy(Question,2,MaxInt)); if Question[1]='*' then begin Res:=GetJsonFromSQL('SELECT * FROM Reponse'); end else Res:=GetJsonFromSQL(formatUTF8('SELECT * FROM Reponse Where ID=?',[],[Question])); end else begin ListeIDMot:=TSQLMotClef.MotsToListeID(self,Question,False,False); ListeIDSoundex:=MotsToListeIDSoundex(Question); SQL:=FormatUTF8( 'SELECT Max(Pertinence) as Pertinence, ID, Titre, IDTypeBase, TypeReponse FROM ( '+ ' SELECT Count(*) as Pertinence, L.IdReponse as ID, R.Titre, R.IdTypeBase, R.TypeReponse '+ ' FROM Lien L, Reponse R '+ ' WHERE (L.IDMotClef in %) and (R.ID=L.IdReponse) '+ ' GROUP BY IdReponse '+ 'UNION '+ ' SELECT Count(*)*.5 as Pertinence, L.IdReponse as ID, R.Titre, R.IdTypeBase, R.TypeReponse '+ ' FROM Lien L, Reponse R '+ ' WHERE (L.IDMotClef in %) and (R.ID=L.IdReponse) '+ ' GROUP BY IdReponse '+ ')',[ListeIDMot,ListeIDSoundex],[]); SQLWhere:=''; if IdTypeBase>0 then SQLWhere:=SQLWhere+FormatUTF8('((IdTypeBase=?) or (IdTypeBase=?) or (IdTypeBase is null)) ',[],[0,IdTypeBase]) else SQLWhere:='(1=1)'; case TypeReponse of ttr_Assistance:; ttr_Tous: SQLWhere:=FormatUTF8('% and (TypeReponse<>?)',[SQLWhere],[Ord(ttr_Assistance)]); ttr_Compte: SQLWhere:=FormatUTF8('% and (TypeReponse=?)',[SQLWhere],[Ord(ttr_Compte)]); ttr_ModelEcriture: SQLWhere:=FormatUTF8('% and (TypeReponse=?)',[SQLWhere],[Ord(ttr_ModelEcriture)]); end; IF SQLWhere<>'' then SQL:=SQL+' WHERE '+SQLWhere; SQL:=SQL+' GROUP BY ID ORDER BY Pertinence DESC'; Res:=GetJsonFromSQL(SQL); end; Ctxt.Returns(Res); end;
Utilise les fonctions suivantes :
GetJsonFromSQL
function TNonoServer.GetJsonFromSQL(SQL:RawUTF8): RawUTF8; var Table:TSQLTableJSON; Doc:Variant; begin Table:=Self.ExecuteList([],SQL); Try Result:=''; If Table.RowCount=0 then Result:='{}' else begin Table.ToDocVariant(doc,True); result:=VariantToUTF8(Doc); end; finally { Quoi qu'il arrive entre le Try et le finally, je libère la mémoire et je passe entre le finally et le end } Table.free; end; end;
MotsToListeID
class function TSQLMotClef.MotsToListeID(Rest: TSQLRest; Chaine: String; AjouteIDSiInconnu: Boolean; AjouteSurServeur: Boolean): String; var Mots:TStringArray; aMot:TSQLMotClef; aID:TID; MotSt:String; i:Integer; begin { La Question est découpée dans un tableau de liste de mots en tenant compte des règles de séparations } Chaine:=MUppercase(Chaine); Mots:=Chaine.Split([' ',';',',','.','!','?','(',')','[',']','{','}']); Result:=''; { On boucle sur chaque mot non vide découpé à l'étape précédente pour le traiter } for i:=0 to High(Mots) do if Mots[i]<>'' then begin MotSt:=Mots[i]; aMot:=TSQLMotClef.create(Rest,FormatUTF8('Mot like ?',[],[MotSt])); Try If aMot.ID=0 then begin If AjouteSurServeur then begin aID:=TSQLMotClef.Ajoute(Rest,MotSt); if AjouteIDSiInconnu then Result:=Result+IntToStr(aID)+','; end; end else begin If not aMot.SansSens then Result:=Result+IntToStr(aMot.ID)+','; end; finally { Quoi qu'il arrive entre le Try et le finally, on libère la mémoire et on passe entre le finally et le end } aMot.free; end; end; Result:=' ('+Copy(Result,1,Length(Result)-1)+')'; writeln('MotsToListeID '+Chaine+' -> '+Result); Setlength(Mots,0); end;
Création du fichier nono.service.ts
→ On déclare la propriété urlBase qui est l’URL d’accès à notre serveur
//URL en exploitataion //public urlBase:string ='https://bases.logeas.fr/root/'; //URL locale public urlBase:string ='http://localhost:8087/root/';
→ On déclare la méthode Recherche
public Recherche(IdTypeBase:number, Question:string): Observable<any> { let URL = this.urlBase + 'Recherche?IdTypeBase='+IdTypeBase+'&Question='+encodeURIComponent(Question); console.log('URL : ',URL); return this.http.get(URL) .pipe( retry(2), catchError(err => this.getError(err)) ); }
→ On gère les erreurs potentielles avec la méthode getError
getError(error: any) { let message = ''; if (error.error instanceof ErrorEvent) { // gère les erreurs côté client message = `Error: ${error.error.message}`; } else { // gère les erreurs côté serveur message = `Error Code: ${error.status}\nMessage: ${error.message}`; } console.log('getError ', message); return throwError(error); }
→ Implémentation et utilisation de la méthode recherche du back
Dans le fichier app.component.ts
→ Import des modules nécessaires
import { Component } from '@angular/core'; import { NonoService, TSQLReponse, TSQLReponses } from 'src/nono.service'; import notify from 'devextreme/ui/notify'; import { OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router';
→ Composant de la librairie DevExtreme du côté HTML
<dx-text-box [(value)]="Question" placeholder="Saisissez votre demande" valueChangeEvent="keyup" (onValueChanged)="onCheckboxValueChanged($event)" [maxLength]="40"> </dx-text-box>
→ Côté TS du composant text-box et de son évènement onValueChanged
// Récupère la valeur de Question pour retourner une Réponse onCheckboxValueChanged(e:any) { this.nono.Recherche(this.IdTypeBase, this.Question).subscribe({ next: (res: any) => {console.log('ICI ',res); this.Reponses=res}, error: (error) => {console.log('ERR ICI ',error);},}); }
→ Affichage des réponses récoltés
→Composant grille de DevExtreme pour les réponses
<!-- ZONE DE LA GRILLE OU SONT LES REPONSES --> <dx-data-grid id="gridContainer" [dataSource]="Reponses" keyExpr="ID" [showBorders]="true" [focusedRowEnabled]="true" [(autoNavigateToFocusedRow)]="autoNavigateToFocusedRow" (onFocusedRowChanged)="onFocusedRowChanged($event)"> <dxi-column dataField="Titre"></dxi-column> </dx-data-grid> <!-- FIN ZONE DE LA GRILLE OU SONT LES REPONSES -->
→ Côté TS du composant data-grid et de son évènement onFocusedRowChanged
// Récupère le texte de la réponse sélectionnée onFocusedRowChanged(e:any) { this.ligneCourante = e.rowIndex; this.ReponseCourante = this.Reponses[this.ligneCourante]; console.log("Avant changement",e.rowIndex) this.nono.GetMotClef(this.ReponseCourante.ID).subscribe({ next: (res: any) => {console.log('ICI ',res); this.motsclefs = res.Liste}, error: (error) => {console.log('ERR ICI ',error);}, }) console.log("Mots Clefs :", this.motsclefs) }
→ Variable et classe
→ Déclaration des variables dans le fichier TS
title = 'nono-angular'; Question = ''; Reponses! : TSQLReponses; ReponseCourante!:TSQLReponse; motsclefs = ''; autoNavigateToFocusedRow = true; ligneCourante: any; isAssistance : boolean = false IdTypeBase : number = 0;
→ Déclaration des classes TSQLReponses et TSQLReponse dans le fichier TS
// Déclaration de la classe TSQLReponses -> Tableau de TSQLReponse export type TSQLReponses = [TSQLReponse]; // Déclaration de la classe TSQLReponse export class TSQLReponse { ID:number = 0; Titre: string = ''; Text: string = ''; IdTypeBase: number = 0; NbUse: number = 0; Createur: string = ''; };