diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ff19b62559832e0afe72cb8f0f289d90c0ebcc5..0d3259356b4c86b90d142af0dc6f88e21e6006b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+6.2.1 (29/03/2024)
+------------------
+
+- Nouveau composant UTableAjax permettant de créer un tableau avec chargement des données en Ajax et affichage avec Vue, avec son côté serveur.
+- Suppression de la fonction de formatage de date
+
6.2.0 (26/03/2024)
------------------
diff --git a/components/UTableAjax.vue b/components/UTableAjax.vue
new file mode 100644
index 0000000000000000000000000000000000000000..75ba19f581e61adceaa3a146b932894cea0faec8
--- /dev/null
+++ b/components/UTableAjax.vue
@@ -0,0 +1,199 @@
+<template>
+ <div class="dt-bootstrap5">
+ <b-row>
+ <b-col>
+ Afficher <label>
+ <select v-model="dSize" class="form-select form-select-sm">
+ <option v-for="ps in pageSizes" :key="ps" :value="ps">{{ ps }}</option>
+ </select>
+ </label> éléments
+ </b-col>
+ <b-col>
+ <div class="float-end">
+ Rechercher : <label><input v-model="dSearch"
+ class="form-control form-inline form-control-sm"/></label>
+ </div>
+ </b-col>
+ </b-row>
+ <table class="table table-bordered dataTable mb-2" ref="tableRef">
+ <slot></slot>
+ </table>
+ <b-row>
+ <b-col>Affichage de l'élément {{ elStart }} à {{ elEnd }} sur {{ dCount }} éléments</b-col>
+ <b-col>
+ <div class="dataTables_paginate paging_simple_numbers">
+ <b-pagination align="end" :page="page" v-model="page" :total-rows="dCount" :per-page="cSize"
+ last-number="true"
+ firstNumber="true"
+ prev-text="Précédent"
+ next-text="Suivant"
+ />
+ </div>
+ </b-col>
+ </b-row>
+ </div>
+</template>
+<script>
+
+export default {
+ name: "UTableAjax",
+ props: {
+ id: {required: false, type: String},
+ size: {required: false, default: 10},
+ count: {required: false},
+ search: {required: false},
+ dataUrl: {required: true, type: String},
+ },
+ data()
+ {
+ return {
+ page: 1,
+ pageSizes: [10, 25, 50, 100, 'Tous'],
+ defaultSize: 10,
+ dSize: this.size,
+ dCount: this.count,
+ dSearch: this.search,
+ searchTimer: null,
+ columns: {},
+ loading: false,
+ orderCol: undefined,
+ orderDir: 'asc',
+ };
+ },
+ computed: {
+ cSize()
+ {
+ if (isNaN(this.dSize)) {
+ return 9999999999999;
+ }
+ return this.dSize;
+ },
+ storageIdentifier()
+ {
+ return 'UTableAjax-' + this.id + '-' + window.location.href;
+ },
+ elStart()
+ {
+ if (isNaN(this.dSize)) {
+ return 1;
+ }
+ return ((this.page - 1) * this.dSize) + 1;
+ },
+ elEnd()
+ {
+ if (isNaN(this.dCount)) {
+ return (this.page - 1) * this.dSize + this.dSize;
+ }
+ if (isNaN(this.dSize)) {
+ return this.dCount;
+ }
+ return Math.min(this.dCount, (this.page - 1) * this.dSize + this.dSize);
+ },
+ },
+ watch: {
+ dSize(newSize)
+ {
+ localStorage.setItem(this.storageIdentifier, newSize);
+ this.getData();
+ },
+ dSearch(newSearch)
+ {
+ const that = this;
+ clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(() => {
+ if (this.page > 1) {
+ this.page = 1;
+ }else {
+ that.getData();
+ }
+ }, 500);
+ },
+ page(newPage)
+ {
+ this.getData();
+ },
+ },
+ methods: {
+ getData()
+ {
+ unicaenVue.axios.post(this.dataUrl, {
+ page: this.page,
+ size: this.dSize,
+ elStart: this.elStart,
+ elEnd: this.elEnd,
+ search: this.dSearch,
+ orderCol: this.orderCol,
+ orderDir: this.orderDir,
+ }).then(response => {
+ let data = response.data;
+ this.dCount = data.count;
+ this.$emit('data', data.data);
+ });
+ },
+ orderBy(column)
+ {
+ const element = this.columns[column];
+ for(let col in this.columns){
+ if (col != column){
+ if (this.columns[col].classList.contains('sorting_asc')){
+ this.columns[col].classList.remove('sorting_asc');
+ }
+ if (this.columns[col].classList.contains('sorting_desc')){
+ this.columns[col].classList.remove('sorting_desc');
+ }
+ console.log(col)
+ }
+ }
+ let newOrder = 'asc';
+
+ if (element.classList.contains('sorting_asc')){
+ newOrder = 'desc';
+ element.classList.remove('sorting_asc');
+ element.classList.add('sorting_desc');
+ }else if (element.classList.contains('sorting_desc')){
+ element.classList.remove('sorting_desc');
+ element.classList.add('sorting_asc');
+ }else{
+ element.classList.add('sorting_asc');
+ }
+
+ this.orderCol = column;
+ this.orderDir = newOrder;
+
+ this.getData();
+ },
+ },
+ mounted()
+ {
+ this.dSize = parseInt(localStorage.getItem(this.storageIdentifier)) || this.defaultSize;
+ this.page = 1;
+ this.dSize = this.size;
+ this.dCount = this.count;
+ const headEl = this.$refs.tableRef;
+ const that = this;
+ if (headEl) {
+ // Récupération des éléments <th> qui ont l'attribut 'column'
+ const thElements = headEl.querySelectorAll('th[column]');
+ thElements.forEach(th => {
+ Array.from(th.attributes).forEach(attr => {
+ if (attr.name == 'column') {
+ this.columns[attr.value] = th;
+ th.dataset.column = attr.value;
+ th.onclick = function () {
+ that.orderBy(this.dataset.column);
+ }
+ th.removeAttribute(attr.name);
+ th.classList.add('sorting');
+ }
+ });
+ });
+ }
+
+ this.getData();
+ },
+}
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/doc/composants.md b/doc/composants.md
new file mode 100644
index 0000000000000000000000000000000000000000..a3c7083a19bd673b48f9e847993491b36a663b0d
--- /dev/null
+++ b/doc/composants.md
@@ -0,0 +1,4 @@
+# Composants proposés en standard
+
+- [UCalendar]() Calendrier pour la saisie d'informations par mois (à documenter)
+- [UTableAjax](composants/UTableAjax.md) Permet d'afficher des tableaux de données chargés en Ajax et formatés avec Vue.js
diff --git a/doc/composants/UTableAjax.md b/doc/composants/UTableAjax.md
new file mode 100644
index 0000000000000000000000000000000000000000..6babbc2b9615076899b1d77cc32445634635deb8
--- /dev/null
+++ b/doc/composants/UTableAjax.md
@@ -0,0 +1,103 @@
+# UTableAjax
+
+Permet de dessiner un pseudo DataTable avec un chargement des données en Ajax et un affichage en VueJS.
+
+Exemple d'utilisation côté client, dans un composant Vue :
+
+```vue
+<template>
+ <u-table-ajax :data-url="this.dataUrl" @data="maj">
+ <thead>
+ <tr>
+ <th column="ID">Id</th><!-- l'attribut column doit être renseigné pour pouvoir la rendre triable -->
+ <th column="LIBELLE">Libellé</th>
+ <th column="FORMULE">Formule</th>
+ <th column="ANNEE">Année</th>
+ <th> </th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- On liste toute les lignes et on les affiche ici -->
+ <tr v-for="(line,i) in lines" :key="i">
+ <td>{{ line['ID'] }}</td>
+ <td>{{ line['LIBELLE'] }}</td>
+ <td>{{ line['FORMULE'] }}</td>
+ <td>{{ line['ANNEE'] }}</td>
+ <td><a :href="editUrl(line['ID'])">Modifier</a></td>
+ </tr>
+ </tbody>
+ </u-table-ajax>
+</template>
+
+<script>
+
+export default {
+ name: 'Test',
+ data()
+ {
+ return {
+ dataUrl: unicaenVue.url('.../data'),
+
+ lines: [],
+ };
+ },
+ methods: {
+ maj(lines)
+ {
+ this.lines = lines;
+ },
+ editUrl(id)
+ {
+ return unicaenVue.url('mon-url/:id', {id: id});
+ },
+ },
+}
+
+</script>
+<style scoped>
+
+</style>
+```
+
+Exemple de génération de données côté serveur, dans un contrôleur :
+```php
+<?php
+
+namespace MonModule\Controller;
+
+use Laminas\Mvc\Controller\AbstractActionController;
+use UnicaenVue\Util;
+
+class TestController extends AbstractActionController
+{
+
+ public function dataAction()
+ {
+ // Requête SQL
+ // Elle doit avoir un paramètre :search pour traiter les recherches
+ // Elle ne doit pas avoir d'OrderBY : il sera ajouté ensuite selon le besoin
+ $sql = "
+ SELECT
+ fti.id id,
+ fti.libelle libelle,
+ f.libelle formule,
+ a.libelle annee
+ FROM
+ formule_test_intervenant fti
+ JOIN formule f ON f.id = fti.formule_id
+ JOIN annee a ON a.id = fti.annee_id
+ WHERE
+ lower(fti.libelle || ' ' || f.libelle || ' ' || a.libelle) like :search
+ ";
+
+ // renvoie un axiosModel avec les données issues du requêtage
+ $em = /* Récupération de l'entityManager de Doctrine */;
+
+ return Util::tableAjaxData($em, $this->axios()->fromPOst(), $sql);
+ }
+}
+```
+
+Résultat :
+
+
\ No newline at end of file
diff --git a/doc/doc.md b/doc/doc.md
index 6bfdcc3d85c1b2304a41afa8ab8b0ca0e1a2419a..6572a9e71d9a8001681bcf76eae20f155cd8641f 100644
--- a/doc/doc.md
+++ b/doc/doc.md
@@ -8,6 +8,8 @@
[Partie serveur PHP](serveur.md)
+[Composants proposés en standard](composants.md)
+
Pour faire vos premiers pas avec Vue :
- [Doc d'introduction à VueJS de Stéphane](https://git.unicaen.fr/bouvry/presentation-dev/-/blob/master/src/vuejs.md)
diff --git a/src/Controller/Plugin/AxiosPlugin.php b/src/Controller/Plugin/AxiosPlugin.php
index 695ecc907e95e51c19a7a84e69deee48f7f50400..2179d4bd02033465e095393dd2bc2b184e7d9bd5 100755
--- a/src/Controller/Plugin/AxiosPlugin.php
+++ b/src/Controller/Plugin/AxiosPlugin.php
@@ -2,13 +2,7 @@
namespace UnicaenVue\Controller\Plugin;
-use Doctrine\Common\Collections\Collection;
-use Doctrine\ORM\Query;
use Laminas\Mvc\Controller\Plugin\AbstractPlugin;
-use Laminas\Mvc\Plugin\FlashMessenger\FlashMessenger;
-use Laminas\View\Model\JsonModel;
-use UnicaenVue\Axios\AxiosExtractor;
-use UnicaenVue\Axios\AxiosExtractorInterface;
/**
* Description of Axios
diff --git a/src/Util.php b/src/Util.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b64f494824a2c93dbe761f94434719c1cc89789
--- /dev/null
+++ b/src/Util.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace UnicaenVue;
+
+use Doctrine\ORM\EntityManager;
+use UnicaenVue\View\Model\AxiosModel;
+
+class Util
+{
+ static public function tableAjaxData(EntityManager $em, array $post, string $sql): AxiosModel
+ {
+ $search = $post['search'] ?? '';
+ $elStart = ($post['elStart'] ?? 1) - 1;
+ $size = $post['size'] ?? 10;
+ $orderCol = $post['orderCol'] ?? null;
+ $orderDir = ($post['orderDir'] ?? 'asc') == 'asc' ? 'asc' : 'desc';
+
+ if ($orderCol && (str_contains($orderCol, '"') || str_contains($orderCol, "'"))){
+ $orderCol = null; // protection contre les injections
+ }
+
+ $limitSql = "OFFSET :elStart ROWS FETCH FIRST :size ROWS ONLY";
+ if ($orderCol) {
+ $orderSql = "ORDER BY $orderCol $orderDir";
+ } else {
+ $orderSql = '';
+ }
+ $params = [
+ 'elStart' => $elStart,
+ 'size' => $size,
+ 'search' => strtolower('%' . $search . '%'),
+ ];
+ $count = (int)$em->getConnection()->fetchOne('SELECT count(*) cc FROM (' . $sql . ') t', $params);
+ $data = $em->getConnection()->fetchAllAssociative($sql . "\n" . $orderSql . "\n" . $limitSql, $params);
+
+ $result = [
+ 'count' => $count,
+ 'data' => $data,
+ ];
+
+ return new AxiosModel($result);
+ }
+}
\ No newline at end of file
diff --git a/src/View/Model/AxiosModel.php b/src/View/Model/AxiosModel.php
index a259898df830dd6c1d3f5dd8f46c89522484498b..8b1756ffde587413383e198cfe87f16fe2ff055a 100644
--- a/src/View/Model/AxiosModel.php
+++ b/src/View/Model/AxiosModel.php
@@ -2,7 +2,6 @@
namespace UnicaenVue\View\Model;
-use Exception;
use Laminas\View\Model\ModelInterface;
use Traversable;