From 93086eba608a2f7f54a5aed15bc56468d0f064ce Mon Sep 17 00:00:00 2001 From: Tykayn Date: Tue, 24 Jun 2025 13:16:48 +0200 Subject: [PATCH] =?UTF-8?q?bubble=20fraicheur=20des=20completions=20ajout?= =?UTF-8?q?=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/app.js | 42 +++++++++++++++------ assets/dashboard-charts.js | 31 +++++++-------- assets/js/table-sortable.js | 8 ++++ assets/js/table-sortable.min.js | 0 assets/table-map-toggles.js | 8 +--- migrations/Version20250624103515.php | 35 +++++++++++++++++ package-lock.json | 17 +++++++++ package.json | 4 ++ src/Controller/AdminController.php | 36 ++++++++++++------ src/Entity/Place.php | 17 +++++++++ src/Service/Motocultrice.php | 27 ++++++------- templates/admin/email_content.html.twig | 2 + templates/admin/labourage_results.html.twig | 2 +- templates/admin/stats.html.twig | 5 ++- templates/public/closed_commerces.html.twig | 2 +- templates/public/dashboard.html.twig | 3 +- templates/public/latest_changes.html.twig | 4 +- templates/public/places_with_note.html.twig | 2 +- 18 files changed, 179 insertions(+), 66 deletions(-) create mode 100644 assets/js/table-sortable.js create mode 100644 assets/js/table-sortable.min.js create mode 100644 migrations/Version20250624103515.php diff --git a/assets/app.js b/assets/app.js index f290bca..97a417a 100644 --- a/assets/app.js +++ b/assets/app.js @@ -34,6 +34,12 @@ import { updateMapHeightForLargeScreens } from './utils.js'; import Tablesort from 'tablesort'; +import TableSort from 'table-sort-js/table-sort.js'; +import $ from 'jquery'; +window.$ = $; +window.jQuery = $; +// Charger table-sortable (version non minifiée locale) +import '../assets/js/table-sortable.js'; window.Chart = Chart; window.genererCouleurPastel = genererCouleurPastel; @@ -44,6 +50,7 @@ window.ChartDataLabels = ChartDataLabels; window.maplibregl = maplibregl; window.toggleCompletionInfo = toggleCompletionInfo; window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens; +window.Tablesort = Tablesort; Chart.register(ChartDataLabels); @@ -112,11 +119,6 @@ document.addEventListener('DOMContentLoaded', () => { } updateCompletionProgress(); - // Activer le tri sur tous les tableaux désignés - document.querySelectorAll('.js-sort-table').forEach(table => { - new Tablesort(table); - }); - // Focus sur le premier champ texte au chargement // const firstTextInput = document.querySelector('input.form-control'); // if (firstTextInput) { @@ -131,13 +133,6 @@ document.addEventListener('DOMContentLoaded', () => { parseCuisine(); - // Tri automatique des tableaux - // const tables = document.querySelectorAll('table'); - // tables.forEach(table => { - // table.classList.add('js-sort-table'); - // }); - - // Modifier la fonction de recherche existante const searchInput = document.getElementById('app_admin_labourer'); const suggestionList = document.getElementById('suggestionList'); @@ -201,4 +196,27 @@ document.addEventListener('DOMContentLoaded', () => { enableLabourageForm(); adjustListGroupFontSize('.list-group-item'); + + // Activer le tri naturel sur tous les tableaux avec la classe table-sort + document.querySelectorAll('table.table-sort').forEach(table => { + new TableSort(table); + }); + + // Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats + if (document.querySelector('#dashboard-table')) { + $('#dashboard-table').tableSortable({ + pagination: false, + showPaginationLabel: true, + searchField: '#dashboard-table-search', + responsive: false + }); + } + if (document.querySelector('#stats-table')) { + $('#stats-table').tableSortable({ + pagination: false, + showPaginationLabel: true, + searchField: '#stats-table-search', + responsive: false + }); + } }); diff --git a/assets/dashboard-charts.js b/assets/dashboard-charts.js index 4b33eac..1e55dbd 100644 --- a/assets/dashboard-charts.js +++ b/assets/dashboard-charts.js @@ -47,16 +47,17 @@ function waitForChartAndDrawBubble() { } // Calcul de la régression linéaire (moindres carrés) - const validPoints = bubbleChartData.filter(d => d.x > 0 && d.y > 0); + // On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion + const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null); const n = validPoints.length; let regressionLine = null, slope = 0, intercept = 0; if (n >= 2) { let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; validPoints.forEach(d => { - sumX += Math.log10(d.x); + sumX += d.x; sumY += d.y; - sumXY += Math.log10(d.x) * d.y; - sumXX += Math.log10(d.x) * Math.log10(d.x); + sumXY += d.x * d.y; + sumXX += d.x * d.x; }); const meanX = sumX / n; const meanY = sumY / n; @@ -65,8 +66,8 @@ function waitForChartAndDrawBubble() { const xMin = Math.min(...validPoints.map(d => d.x)); const xMax = Math.max(...validPoints.map(d => d.x)); regressionLine = [ - { x: xMin, y: slope * Math.log10(xMin) + intercept }, - { x: xMax, y: slope * Math.log10(xMax) + intercept } + { x: xMin, y: slope * xMin + intercept }, + { x: xMax, y: slope * xMax + intercept } ]; } window.Chart.register(window.ChartDataLabels); @@ -105,10 +106,8 @@ function waitForChartAndDrawBubble() { ].filter(Boolean) }, options: { - // responsive: true, plugins: { datalabels: { - // Désactivé au niveau global, activé par dataset display: false }, legend: { display: true }, @@ -117,14 +116,14 @@ function waitForChartAndDrawBubble() { label: (context) => { const d = context.raw; if (context.dataset.type === 'line') { - return `Régression: y = ${slope.toFixed(2)} × log10(x) + ${intercept.toFixed(2)}`; + return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`; } return [ `${d.label}`, - `Population: ${d.x.toLocaleString()}`, - `Nombre de lieux: ${d.r.toFixed(2)}`, - `Complétion: ${d.y.toFixed(2)}%`, `Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`, + `Complétion: ${d.y.toFixed(2)}%`, + `Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`, + `Nombre de lieux: ${d.r.toFixed(2)}`, `Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`, `Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`, `Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}` @@ -135,11 +134,13 @@ function waitForChartAndDrawBubble() { }, scales: { x: { - type: 'logarithmic', - title: { display: true, text: 'Population (échelle log)' } + type: 'linear', + title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' } }, y: { - title: { display: true, text: 'Completion' } + title: { display: true, text: 'Taux de complétion (%)' }, + min: 0, + max: 100 } } } diff --git a/assets/js/table-sortable.js b/assets/js/table-sortable.js new file mode 100644 index 0000000..58ecd8f --- /dev/null +++ b/assets/js/table-sortable.js @@ -0,0 +1,8 @@ +/* + * table-sortable + * version: 2.0.3 + * release date: 4/2/2021 + * (c) Ravi Dhiman https://ravid.dev + * For the full copyright and license information, please view the LICENSE +*/ +!function(t){var e={};function n(a){if(e[a])return e[a].exports;var i=e[a]={i:a,l:!1,exports:{}};return t[a].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,a){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(a,i,function(e){return t[e]}.bind(null,i));return a},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([function(t,e){t.exports=jQuery},function(t,e,n){t.exports=n(3)},function(t,e,n){},function(t,e,n){"use strict";n.r(e);var a={};function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function r(t,e){for(var n=0;n2?a-2:0),r=2;rparseFloat(e)?1:-1;if(g(t)){var n=new Date(t),a=new Date(e);return n.getTime()>a.getTime()?1:-1}return p(t)?t>e?1:-1:1}return 0},k=function(t,e){var n;return function(){var a=this,i=arguments;clearTimeout(n),n=window.setTimeout((function(){return t.apply(a,i)}),e)}},C=function(t){return p(t)?t.toLowerCase():String(t)},E=function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[];if(!e&&!h(e))return!1;var a=m(t);c(n)||(n=a);for(var i=0,r=a.length,o=!1,s=C(e);i-1&&u.indexOf(s)>-1){o=!0;break}if(!n.length&&u.indexOf(s)>-1){o=!0;break}i+=1}return o},w=function(t,e){return c(e)&&e[0]<=t&&e[1]>t},D=function(){function t(){i(this,t),this._name="dataset",this.dataset=null,this._cachedData=null,this._datasetLen=0,this._outLen=10,this.sortDirection={ASC:"asc",DESC:"desc"}}return o(t,[{key:"_formatError",value:function(t,e,n){for(var i=arguments.length,r=new Array(i>3?i-3:0),o=3;oe),"get",'"from" cannot be greater than "to"'),t=Math.max(t,0),e=Math.min(e,this._datasetLen),this.dataset.slice(t,e)}},{key:"sort",value:function(t,e){this._hasDataset(),this._formatError(p(t),"sort",'Requires "column" type of string'),this._formatError(p(e),"sort",'Requires "direction" type of string'),this._formatError("asc"===e||"desc"===e,"sort",'"%s" is invalid sort direction. Use "dataset.sortDirection.ASC" or "dataset.sortDirection.DESC".',e);var n=this.top(1)[0];return this._formatError("undefined"!==typeof n[t],"sort",'Column name "%s" does not exist in collection',t),this.sortDirection.ASC===e?this.dataset.sort((function(e,n){return P(e[t],n[t])})):this.dataset.sort((function(e,n){return P(n[t],e[t])})),this.top(this._datasetLen)}},{key:"pushData",value:function(t){c(t)&&Array.prototype.push.apply(this.dataset,t)}},{key:"lookUp",value:function(t,e){if(p(t)||h(t)){var n=JSON.parse(this._cachedData);this.dataset=""===t?n:v(n,(function(n){return E(n,t,e)})),this._datasetLen=this.dataset.length}}}]),t}(),x=function(t,e,n){return{node:t,attrs:e,children:n}},S=function(t){var e=t.node,n=t.attrs;t.children;return function(t,e){if(!e)return t;for(var n=Object.keys(e),a=0;a")),n)},U=function t(e,n,a){if(a||n.empty(),c(e)){for(var i=[],r=0;r\u25bc",desc:"\u25b2"},nextText:"Next",prevText:"Prev",tableWillMount:function(){},tableDidMount:function(){},tableWillUpdate:function(){},tableDidUpdate:function(){},tableWillUnmount:function(){},tableDidUnmount:function(){},onPaginationChange:null},this._styles=null,this._dataset=null,this._table=null,this._thead=null,this._tbody=null,this._isMounted=!1,this._isUpdating=!1,this._sorting={currentCol:"",dir:""},this._pagination={elm:null,currentPage:0,totalPages:1,visiblePageNumbers:5},this._cachedOption=null,this._cachedViewPort=-1,this.setData=function(t,e,a){n.logError(c(t),"setData","expect first argument as array of objects"),n.logError(d(e),"setData","expect second argument as objects"),n._isMounted&&t&&(a?n._dataset.pushData(t):n._dataset.fromCollection(t),e&&(n.options.columns=e),n.refresh())},this.getData=function(){return n._isMounted?n._dataset.top():[]},this.getCurrentPageData=function(){if(n._isMounted){var t=n.options.rowsPerPage,e=n._pagination.currentPage*t,a=e+t;return n._dataset.get(e,a)}return[]},this.refresh=function(t){t?(n.distroy(),n.create()):n._isMounted&&n.updateTable()},this.distroy=function(){n._isMounted&&(n.emitLifeCycles("tableWillUnmount"),n._table.remove(),n._styles&&n._styles.length&&(n._styles.remove(),n._styles=null),n._dataset=null,n._table=null,n._thead=null,n._tbody=null,n._pagination.elm&&n._pagination.elm.remove(),n._pagination={elm:null,currentPage:0,totalPages:0,visiblePageNumbers:5},n._isMounted=!1,n._isUpdating=!1,n._sorting={currentCol:"",dir:""},n._cachedViewPort=-1,n._cachedOption=null,n.emitLifeCycles("tableDidUnmount"))},this.create=function(){n._isMounted||n.init()},this.options=u.a.extend(this._defOptions,e),delete this._defOptions,this._rootElement=u()(this.options.element),this.engine=L(),this.optionDepreciation(),this.init(),this._debounceUpdateTable()}return o(t,[{key:"optionDepreciation",value:function(){var t=this.options;this.logWarn(t.columnsHtml,"columnsHtml","has been deprecated. Use formatHeader()"),this.logWarn(t.processHtml,"processHtml","has been deprecated. Use formatCell()"),this.logWarn(t.dateParsing,"dateParsing","has been deprecated. It is true by default."),this.logWarn(t.generateUniqueIds,"generateUniqueIds","has been deprecated. It is true by default."),this.logWarn(t.showPaginationLabel,"showPaginationLabel","has been deprecated. It is true by default."),this.logWarn(t.paginationLength,"paginationLength","has been deprecated. Use rowsPerPage")}},{key:"logError",value:function(t,e,n){for(var i=arguments.length,r=new Array(i>3?i-3:0),o=3;o1?n-1:0),i=1;i1&&void 0!==arguments[1]?arguments[1]:[],n=this.options.columns;this.logError(e&&c(e),"lookUp","second argument must be array of keys"),e.length||(e=n),this._pagination.currentPage=0,this._dataset.lookUp(t,m(e)),this.debounceUpdateTable()}},{key:"_bindSearchField",value:function(){var t=this,e=this.options.searchField;if(e){var n=u()(e);this.logError(n.length,"searchField",'"%s" is not a valid DOM element or string',n),n.on("input",(function(){var e=u()(this).val();t.lookUp(e)})),this.options.searchField=n}}},{key:"_validateRootElement",value:function(){this.logError(this._rootElement.length,"element",'"%s" is not a valid root element',this._rootElement)}},{key:"_createTable",value:function(){this._table=u()("
").addClass("table gs-table")}},{key:"_initDataset",value:function(){var t=this.options.data;this.logError(c(t),"data","table-sortable only supports collections. Like: [{ key: value }, { key: value }]");var e=new D;e.fromCollection(t),this._dataset=e}},{key:"_validateColumns",value:function(){var t=this.options.columns;this.logError(d(t),"columns","Invalid column type, see docs")}},{key:"sortData",value:function(t){var e=this._sorting,n=e.dir,a=e.currentCol;t!==a&&(n=""),n?n===this._dataset.sortDirection.ASC?n=this._dataset.sortDirection.DESC:n===this._dataset.sortDirection.DESC&&(n=this._dataset.sortDirection.ASC):n=this._dataset.sortDirection.ASC,a=t,this._sorting={dir:n,currentCol:a},this._dataset.sort(a,n),this.updateCellHeader()}},{key:"_addColSorting",value:function(t,e){var n=this,a=this.options.sorting,i=this;return a?(a&&!c(a)&&((t=u()(t)).attr("role","button"),t.addClass("gs-button"),e===this._sorting.currentCol&&this._sorting.dir&&t.append(this.options.sortingIcons[this._sorting.dir]),t.click((function(t){i.sortData(e)}))),c(a)&&b(a,(function(a){e===a&&((t=u()(t)).attr("role","button"),t.addClass("gs-button"),e===n._sorting.currentCol&&n._sorting.dir&&t.append(n.options.sortingIcons[n._sorting.dir]),t.click((function(t){i.sortData(e)})))})),t):t}},{key:"getCurrentPageIndex",value:function(){var t=this._dataset._datasetLen,e=this.options,n=e.pagination,a=e.rowsPerPage,i=this._pagination.currentPage;if(!n)return{from:0};var r=i*a,o=Math.min(r+a,t);return{from:r=Math.min(r,o),to:o}}},{key:"_renderHeader",value:function(t){var e=this;t||(t=u()(''));var n=this.options,a=n.columns,i=n.formatHeader,r=[],o=m(a);b(o,(function(t,n){var o=a[t];f(i)&&(o=i(a[t],t,n)),o=e._addColSorting(u()("").html(o),t);var s=e.engine.createElement("th",{html:o});r.push(s)}));var s=this.engine.createElement("tr",null,r);return this.engine.render(s,t)}},{key:"_renderBody",value:function(t){t||(t=u()(''));var e=this.engine,n=this.options,a=n.columns,i=n.formatCell,r=this.getCurrentPageIndex(),o=r.from,s=r.to,l=[];l=void 0===s?this._dataset.top():this._dataset.get(o,s);var c=[],h=m(a);return b(l,(function(t,n){var a=[];b(h,(function(n){var r;void 0!==t[n]&&(r=f(i)?e.createElement("td",{html:i(t,n)}):e.createElement("td",{html:t[n]}),a.push(r))})),c.push(e.createElement("tr",null,a))})),e.render(c,t)}},{key:"_createCells",value:function(){return{thead:this._renderHeader(),tbody:this._renderBody()}}},{key:"onPaginationBtnClick",value:function(t,e){var n=this,a=this._pagination,i=a.totalPages,r=a.currentPage,o=this.options.onPaginationChange;"up"===t?r=0&&(r-=1);if(f(o)){var s=isNaN(e)?r:e;o.apply(this,[s,function(t){return n.setPage(t)}])}else this._pagination.currentPage=void 0!==e?e:r,this.updateTable()}},{key:"renderPagination",value:function(t){var e=this,n=this.engine,a=this.options,i=a.pagination,r=a.paginationContainer,o=a.prevText,s=a.nextText,l=this._pagination,c=l.currentPage,h=l.totalPages,d=l.visiblePageNumbers,f=Math.min(h,d),p=0,g=Math.min(h,p+f);if(c>f/2&&ch-f/2&&(p=h-f,g=h),t||(t=u()('
'),u()(r).length?u()(r).append(t):this._table.after(t)),!i)return t;var _=[],m=n.createElement("button",{className:"btn btn-default",html:o,disabled:0===c,onClick:function(){return e.onPaginationBtnClick("down")}});_.push(m);var b=n.createElement("button",{className:"btn btn-default",disabled:!0,text:"..."});c>f/2&&_.push(b);for(var v=p;v=h-1,onClick:function(){return e.onPaginationBtnClick("up")}});_.push(P),t.append(_);var k=this.getCurrentPageIndex(),C=k.from,E=k.to,w=n.createElement("span",{text:"Showing ".concat(Math.min(E,C+1)," to ").concat(E," of ").concat(this._dataset._datasetLen," rows")}),D=n.createElement("div",{className:"col-md-6"},w),x=n.createElement("div",{className:"btn-group d-flex justify-content-end"},_),S=n.createElement("div",{className:"col-md-6"},x),U=n.createElement("div",{className:"row"},[D,S]);return n.render(U,t)}},{key:"createPagination",value:function(){var t=this.options,e=t.rowsPerPage,n=t.pagination,a=t.totalPages;if(!n)return!1;this.logError(e&&h(e),"rowsPerPage","should be a number greater than zero."),this.logError(h(a),"totalPages","should be a number greater than zero.");var i=a||Math.ceil(this._dataset._datasetLen/e);0>=i&&(i=1),this._pagination.totalPages=i,this._pagination.elm?this.renderPagination(this._pagination.elm):this._pagination.elm=this.renderPagination()}},{key:"_generateTable",value:function(t,e){this._table.html(""),this._table.append(t),this._table.append(e),this._thead=t,this._tbody=e}},{key:"_renderTable",value:function(){if(this._rootElement.is("table"))this._rootElement.html(this._table.html());else{var t=this.engine.createElement("div",{className:"gs-table-container",append:this._table});this._rootElement=this.engine.render(t,this._rootElement)}}},{key:"_initStyles",value:function(){if(!this.options.responsive){var t=u()("");t.attr("id","gs-table"),t.html(".gs-table-container .table{table-layout:fixed}@media(max-width:767px){.gs-table-container{overflow:auto;max-width:100%}}"),u()("head").append(t),this._styles=t}}},{key:"init",value:function(){this.emitLifeCycles("tableWillMount"),this._validateRootElement(),this._initDataset(),this._createTable(),this._validateColumns();var t=this._createCells(),e=t.thead,n=t.tbody;this._generateTable(e,n),this._renderTable(),this.createPagination(),this._bindSearchField(),this._initStyles(),this._isMounted=!0,this.emitLifeCycles("tableDidMount"),-1===this._cachedViewPort&&this.resizeSideEffect()}},{key:"_debounceUpdateTable",value:function(){this.debounceUpdateTable=k(this.updateTable,400)}},{key:"updateTable",value:function(){this._isUpdating||(this.emitLifeCycles("tableWillUpdate"),this._isUpdating=!0,this._renderHeader(this._thead),this._renderBody(this._tbody),this.createPagination(),this._isUpdating=!1,this.emitLifeCycles("tableDidUpdate"))}},{key:"updateCellHeader",value:function(){this._isUpdating||(this._isUpdating=!0,this.emitLifeCycles("tableWillUpdate"),this._renderHeader(this._thead),this._renderBody(this._tbody),this._isUpdating=!1,this.emitLifeCycles("tableDidUpdate"))}},{key:"resizeSideEffect",value:function(){var t=k(this.makeResponsive,500);window.addEventListener("resize",t.bind(this)),this.makeResponsive()}},{key:"makeResponsive",value:function(){var t,e=this.options.responsive,n=window.innerWidth,a=_(m(e),"desc");if(this.logError(d(e),"responsive",'Invalid type of responsive option provided: "%s"',e),b(a,(function(e){parseInt(e,10)>n&&(t=e)})),this._cachedViewPort!==t){this._cachedViewPort=t;var i=e[t];d(i)?(this._cachedOption||(this._cachedOption=u.a.extend({},this.options)),this.options=u.a.extend(this.options,i),this.refresh()):this._cachedOption&&(this.options=u.a.extend({},this._cachedOption),this._cachedOption=null,this._cachedViewPort=-1,this.refresh())}}}]),t}());window.Pret=L(),window.TableSortable=O,window.DataSet=D,(s=jQuery).fn.tableSortable=function(t){return t.element=s(this),new window.TableSortable(t)};e.default=O}]); \ No newline at end of file diff --git a/assets/js/table-sortable.min.js b/assets/js/table-sortable.min.js new file mode 100644 index 0000000..e69de29 diff --git a/assets/table-map-toggles.js b/assets/table-map-toggles.js index 1f1c6cf..5f4fcf4 100644 --- a/assets/table-map-toggles.js +++ b/assets/table-map-toggles.js @@ -1,11 +1,7 @@ // Gestion du tri des tableaux -import Tablesort from 'tablesort'; +// import Tablesort from 'tablesort'; document.addEventListener('DOMContentLoaded', () => { - document.querySelectorAll('.js-sort-table').forEach(table => { - new Tablesort(table); - }); - // Gestion du toggle gouttes/ronds sur la carte const toggle = document.getElementById('toggleMarkers'); if (toggle && window.updateMarkers) { @@ -18,6 +14,6 @@ document.addEventListener('DOMContentLoaded', () => { // Exposer une fonction pour (ré)appliquer le tri si besoin export function applyTableSort() { document.querySelectorAll('.js-sort-table').forEach(table => { - new Tablesort(table); + new window.Tablesort(table); }); } \ No newline at end of file diff --git a/migrations/Version20250624103515.php b/migrations/Version20250624103515.php new file mode 100644 index 0000000..3a62e23 --- /dev/null +++ b/migrations/Version20250624103515.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE place ADD email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, ADD place_count INT DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE place DROP email_content, DROP place_count + SQL); + } +} diff --git a/package-lock.json b/package-lock.json index 39b892c..953c42e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "license": "UNLICENSED", + "dependencies": { + "jquery": "^3.7.1" + }, "devDependencies": { "@babel/core": "^7.17.0", "@babel/preset-env": "^7.16.0", @@ -14,6 +17,7 @@ "core-js": "^3.38.0", "maplibre-gl": "^5.6.0", "regenerator-runtime": "^0.13.9", + "table-sort-js": "^1.22.2", "tablesort": "^5.6.0", "webpack": "^5.74.0", "webpack-cli": "^5.1.0" @@ -3904,6 +3908,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5491,6 +5501,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/table-sort-js": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/table-sort-js/-/table-sort-js-1.22.2.tgz", + "integrity": "sha512-KUpmoYWH1TCnyiylE0HMCtMeAisl0KYBFjZfBL3CPHOlnhA8jy+RFfZbH6DwCpXAvmK73vsDAX54hg9J4DhuRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tablesort": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.6.0.tgz", diff --git a/package.json b/package.json index d3f21c8..8e5215b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "core-js": "^3.38.0", "maplibre-gl": "^5.6.0", "regenerator-runtime": "^0.13.9", + "table-sort-js": "^1.22.2", "tablesort": "^5.6.0", "webpack": "^5.74.0", "webpack-cli": "^5.1.0" @@ -19,5 +20,8 @@ "dev": "encore dev", "watch": "encore dev --watch", "build": "encore production --progress" + }, + "dependencies": { + "jquery": "^3.7.1" } } diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index c7d5aba..1a2915e 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use function uuid_create; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\JsonResponse; +use Twig\Environment; final class AdminController extends AbstractController { @@ -23,7 +24,8 @@ final class AdminController extends AbstractController public function __construct( private EntityManagerInterface $entityManager, private Motocultrice $motocultrice, - private BudgetService $budgetService + private BudgetService $budgetService, + private Environment $twig ) { } @@ -99,6 +101,10 @@ final class AdminController extends AbstractController $this->entityManager->persist($place); $stats->addPlace($place); $processedCount++; + + // Générer le contenu de l'email avec le template + $emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]); + $place->setEmailContent($emailContent); } elseif ($updateExisting) { // Mettre à jour les données depuis Overpass uniquement si updateExisting est true $existingPlace->update_place_from_overpass_data($placeData); @@ -443,6 +449,8 @@ final class AdminController extends AbstractController $overpass_osm_ids = array_map(fn($place) => $place['id'], $places_overpass); + $batchSize = 200; + $i = 0; foreach ($places_overpass as $placeData) { // Vérifier si le lieu existe déjà (optimisé) $existingPlace = $placesByOsmId[$placeData['id']] ?? null; @@ -471,6 +479,10 @@ final class AdminController extends AbstractController $this->entityManager->persist($place); $stats->addPlace($place); $processedCount++; + + // Générer le contenu de l'email avec le template + $emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]); + $place->setEmailContent($emailContent); } elseif ($updateExisting) { $existingPlace->setDead(false); $existingPlace->update_place_from_overpass_data($placeData); @@ -478,19 +490,19 @@ final class AdminController extends AbstractController $this->entityManager->persist($existingPlace); $updatedCount++; } - } - - // Supprimer les lieux qui ne sont plus dans la réponse Overpass, si activé - if ($deleteMissing) { - $db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); - foreach ($db_places as $db_place) { - if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) { - $this->entityManager->remove($db_place); - $deletedCount++; - } + $i++; + // Flush/clear Doctrine tous les X lieux pour éviter l'explosion mémoire + if (($i % $batchSize) === 0) { + $this->entityManager->flush(); + $this->entityManager->clear(); + // Recharger les stats après clear + $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); } } - + // Flush final + $this->entityManager->flush(); + $this->entityManager->clear(); + // Récupérer tous les commerces de la zone qui n'ont pas été supprimés $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); diff --git a/src/Entity/Place.php b/src/Entity/Place.php index 984415c..1f77238 100644 --- a/src/Entity/Place.php +++ b/src/Entity/Place.php @@ -120,6 +120,12 @@ class Place #[ORM\Column(nullable: true)] private ?int $osm_changeset = null; + #[ORM\Column(type: Types::TEXT, nullable: true, options: ['charset' => 'utf8mb4'])] + private ?string $emailContent = null; + + #[ORM\Column(type: Types::INTEGER, nullable: true)] + private ?int $place_count = null; + public function getPlaceTypeName(): ?string { if ($this->main_tag == 'amenity=restaurant') { @@ -734,4 +740,15 @@ class Place return $this; } + + public function getEmailContent(): ?string + { + return $this->emailContent; + } + + public function setEmailContent(?string $emailContent): static + { + $this->emailContent = $emailContent; + return $this; + } } diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index 11ef320..a07f217 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -252,16 +252,21 @@ out meta;'; } public function get_city_osm_from_zip_code($zip_code) { + // Détection spéciale pour Paris, Lyon, Marseille + if (preg_match('/^75(0[1-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-9])$/', $zip_code)) { + $arr = intval(substr($zip_code, 2, 3)); + return 'Paris ' . $arr . 'e arr.'; + } + if (preg_match('/^69(0[1-9]|1[0-9]|2[0-9])$/', $zip_code)) { + $arr = intval(substr($zip_code, 2, 3)); + return 'Lyon ' . $arr . 'e arr.'; + } + if (preg_match('/^13(0[1-9]|1[0-6])$/', $zip_code)) { + $arr = intval(substr($zip_code, 2, 3)); + return 'Marseille ' . $arr . 'e arr.'; + } // Requête Overpass pour obtenir la zone administrative de niveau 8 avec un nom - $query = "[out:json][timeout:25]; - area[\"ref:INSEE\"=\"{$zip_code}\"]->.searchArea; - ( - relation[\"admin_level\"=\"8\"][\"name\"][\"type\"=\"boundary\"][\"boundary\"=\"administrative\"](area.searchArea); - ); - out body; - >; - out skel qt;"; - + $query = "[out:json][timeout:25];\n area[\"ref:INSEE\"=\"{$zip_code}\"]->.searchArea;\n (\n relation[\"admin_level\"=\"8\"][\"name\"][\"type\"=\"boundary\"][\"boundary\"=\"administrative\"](area.searchArea);\n );\n out body;\n >;\n out skel qt;"; $response = $this->client->request('POST', $this->overpassApiUrl, [ 'body' => ['data' => $query] ]); @@ -389,10 +394,6 @@ out meta;'; - public static function uuid_create_static() { - return $this->uuid_create(); - } - public function uuid_create() { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', // 32 bits for "time_low" diff --git a/templates/admin/email_content.html.twig b/templates/admin/email_content.html.twig index 069507e..4179a36 100644 --- a/templates/admin/email_content.html.twig +++ b/templates/admin/email_content.html.twig @@ -28,5 +28,7 @@ En vous souhaitant une bonne journée.

+{% if place.id %} Ne plus être sollicité pour mettre à jour mon commerce +{% endif %} \ No newline at end of file diff --git a/templates/admin/labourage_results.html.twig b/templates/admin/labourage_results.html.twig index 93de459..a3bc865 100644 --- a/templates/admin/labourage_results.html.twig +++ b/templates/admin/labourage_results.html.twig @@ -25,7 +25,7 @@ commerces existants déjà en base: {{ commerces|length }}

{# {{ dump(commerces[0]) }} #} - +
{% include 'admin/stats/table-head.html.twig' %} diff --git a/templates/admin/stats.html.twig b/templates/admin/stats.html.twig index b173126..d04dc71 100644 --- a/templates/admin/stats.html.twig +++ b/templates/admin/stats.html.twig @@ -54,7 +54,7 @@ {{ stats.name }} - {{ stats.completionPercent }}% complété
- Labourer les mises à jour + Labourer les mises à jour @@ -262,7 +262,8 @@
-
+ +
{% include 'admin/stats/table-head.html.twig' %} {% for commerce in stats.places %} diff --git a/templates/public/closed_commerces.html.twig b/templates/public/closed_commerces.html.twig index b6fd256..c656507 100644 --- a/templates/public/closed_commerces.html.twig +++ b/templates/public/closed_commerces.html.twig @@ -18,7 +18,7 @@

Commerces fermés

Voici la liste des commerces fermés :

-
+
diff --git a/templates/public/dashboard.html.twig b/templates/public/dashboard.html.twig index 66a75cc..7b8aa51 100644 --- a/templates/public/dashboard.html.twig +++ b/templates/public/dashboard.html.twig @@ -137,8 +137,9 @@

Statistiques par ville

+
-
Nom du commerce
+
diff --git a/templates/public/latest_changes.html.twig b/templates/public/latest_changes.html.twig index 3710859..e8ea163 100644 --- a/templates/public/latest_changes.html.twig +++ b/templates/public/latest_changes.html.twig @@ -9,7 +9,7 @@

Lieux modifiés

-
Ville
+
@@ -33,7 +33,7 @@

Lieux affichés

-
Nom
+
diff --git a/templates/public/places_with_note.html.twig b/templates/public/places_with_note.html.twig index 8fe169e..8f3c6d7 100644 --- a/templates/public/places_with_note.html.twig +++ b/templates/public/places_with_note.html.twig @@ -5,7 +5,7 @@ {% block body %}

Commerces avec une note

-
Nom
+
Nom