mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
bubble fraicheur des completions ajouté
This commit is contained in:
parent
cd8369d08c
commit
93086eba60
18 changed files with 179 additions and 66 deletions
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
assets/js/table-sortable.js
Normal file
8
assets/js/table-sortable.js
Normal file
File diff suppressed because one or more lines are too long
0
assets/js/table-sortable.min.js
vendored
Normal file
0
assets/js/table-sortable.min.js
vendored
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
35
migrations/Version20250624103515.php
Normal file
35
migrations/Version20250624103515.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250624103515 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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);
|
||||
}
|
||||
}
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -28,5 +28,7 @@ En vous souhaitant une bonne journée.
|
|||
<br>
|
||||
<hr>
|
||||
|
||||
{% if place.id %}
|
||||
<a href="{{ path('app_admin_commerce', {'id': place.id}) }}">Ne plus être sollicité pour mettre à jour mon commerce</a>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -25,7 +25,7 @@ commerces existants déjà en base: {{ commerces|length }}
|
|||
</p>
|
||||
{# {{ dump(commerces[0]) }} #}
|
||||
|
||||
<table class="table table-striped js-sort-table">
|
||||
<table class="table table-striped table-sort">
|
||||
{% include 'admin/stats/table-head.html.twig' %}
|
||||
|
||||
<tbody>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 0}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<button id="openInJOSM" class="btn btn-secondary ms-2">
|
||||
<i class="bi bi-map"></i> Ouvrir dans JOSM
|
||||
</button>
|
||||
|
@ -262,7 +262,8 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
|
||||
<input type="text" id="stats-table-search" class="form-control mb-2" placeholder="Filtrer les lieux...">
|
||||
<table id="stats-table" class="table table-bordered table-striped table-hover table-responsive table-sort">
|
||||
{% include 'admin/stats/table-head.html.twig' %}
|
||||
<tbody>
|
||||
{% for commerce in stats.places %}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<h1>Commerces fermés</h1>
|
||||
<p>Voici la liste des commerces fermés :</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped js-sort-table">
|
||||
<table class="table table-striped table-sort">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom du commerce</th>
|
||||
|
|
|
@ -137,8 +137,9 @@
|
|||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h2>Statistiques par ville</h2>
|
||||
<input type="text" id="dashboard-table-search" class="form-control mb-2" placeholder="Filtrer les villes...">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped js-sort-table">
|
||||
<table id="dashboard-table" class="table table-striped table-sort">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ville</th>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="row">
|
||||
<div class="col-12 col-md-6 ">
|
||||
<h2>Lieux modifiés</h2>
|
||||
<table class="table table-striped table-hover table-responsive js-sort-table">
|
||||
<table class="table table-striped table-hover table-responsive table-sort">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="col-12 col-md-6 ">
|
||||
<h2>Lieux affichés</h2>
|
||||
<table class="table table-striped table-hover table-responsive js-sort-table">
|
||||
<table class="table table-striped table-hover table-responsive table-sort">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block body %}
|
||||
<div class="container">
|
||||
<h1>Commerces avec une note</h1>
|
||||
<table class="table table-striped js-sort-table table-hover table-responsive">
|
||||
<table class="table table-striped table-hover table-responsive table-sort">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue