mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-06-20 01:44:42 +02:00
compute stats for completion by zone, have base tags, split categories
This commit is contained in:
parent
f15fec6d18
commit
f69b7824af
16 changed files with 1257 additions and 118 deletions
59
migrations/Version20250526200731.php
Normal file
59
migrations/Version20250526200731.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?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 Version20250526200731 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 has_opening_hours BOOLEAN DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ADD has_address BOOLEAN DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ADD has_website BOOLEAN DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ADD has_wheelchair BOOLEAN DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ADD has_note BOOLEAN 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 has_opening_hours
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place DROP has_address
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place DROP has_website
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place DROP has_wheelchair
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place DROP has_note
|
||||
SQL);
|
||||
}
|
||||
}
|
59
migrations/Version20250526203604.php
Normal file
59
migrations/Version20250526203604.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?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 Version20250526203604 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 stats ADD avec_horaires SMALLINT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD avec_adresse SMALLINT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD avec_site SMALLINT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD avec_accessibilite SMALLINT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD avec_note SMALLINT NOT 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 stats DROP avec_horaires
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP avec_adresse
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP avec_site
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP avec_accessibilite
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP avec_note
|
||||
SQL);
|
||||
}
|
||||
}
|
328
public/assets/css/global.css
Normal file
328
public/assets/css/global.css
Normal file
|
@ -0,0 +1,328 @@
|
|||
:root {
|
||||
--primary-color: #2c3e50;
|
||||
--secondary-color: #3498db;
|
||||
--accent-color: #e74c3c;
|
||||
--text-color: #333;
|
||||
--light-gray: #f5f6fa;
|
||||
--border-color: #dcdde1;
|
||||
--success-color: #27ae60;
|
||||
--warning-color: #f1c40f;
|
||||
--error-color: #c0392b;
|
||||
}
|
||||
|
||||
/* Reset et styles de base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background-color: var(--primary-color);
|
||||
padding: 1rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #2c3e50;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: var(--success-color);
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
color: var(--warning-color);
|
||||
border: 1px solid #ffeeba;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
color: var(--error-color);
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.grid-2,
|
||||
.grid-3,
|
||||
.grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-menu.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--primary-color);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Map container */
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Tags display */
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: var(--light-gray);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
opacity: 1;
|
||||
}
|
|
@ -6,7 +6,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Entity\Place;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use function uuid_create;
|
||||
|
@ -29,6 +29,48 @@ final class AdminController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/stats/{zip_code}', name: 'app_admin_stats')]
|
||||
public function calculer_stats(string $zip_code): Response
|
||||
{
|
||||
// Récupérer tous les commerces de la zone
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]);
|
||||
|
||||
// Récupérer les stats existantes pour la zone
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
|
||||
if(!$stats) {
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zip_code);
|
||||
}
|
||||
|
||||
// Calculer les statistiques
|
||||
$calculatedStats = $this->motocultrice->calculateStats($commerces);
|
||||
|
||||
// Mettre à jour les stats pour la zone donnée
|
||||
$stats->setPlacesCount($calculatedStats['places_count']);
|
||||
$stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']);
|
||||
$stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']);
|
||||
$stats->setAvecSite($calculatedStats['counters']['avec_site']);
|
||||
$stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']);
|
||||
$stats->setAvecNote($calculatedStats['counters']['avec_note']);
|
||||
$stats->setCompletionPercent($calculatedStats['completion_percent']);
|
||||
|
||||
// Associer les stats à chaque commerce
|
||||
foreach ($commerces as $commerce) {
|
||||
$commerce->setStats($stats);
|
||||
$this->entityManager->persist($commerce);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->render('admin/stats.html.twig', [
|
||||
'stats' => $stats,
|
||||
'zip_code' => $zip_code,
|
||||
'counters' => $calculatedStats['counters']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/labourer/{zip_code}', name: 'app_admin_labourer')]
|
||||
public function labourer_zone(string $zip_code): Response
|
||||
{
|
||||
|
@ -39,10 +81,71 @@ final class AdminController extends AbstractController
|
|||
|
||||
// Récupérer les commerces existants dans la base de données pour cette zone
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]);
|
||||
|
||||
// Récupérer ou créer les stats pour cette zone
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
|
||||
if (!$stats) {
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zip_code);
|
||||
|
||||
// for commerce, set stats
|
||||
foreach ($commerces as $commerce) {
|
||||
$commerce->setStats($stats);
|
||||
$this->entityManager->persist($commerce);
|
||||
$stats->addPlace($commerce);
|
||||
}
|
||||
|
||||
// rebuild et persist
|
||||
$stats->computeCompletionPercent();
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
// Initialiser les compteurs
|
||||
$counters = [
|
||||
'avec_horaires' => 0,
|
||||
'avec_adresse' => 0,
|
||||
'avec_site' => 0,
|
||||
'avec_accessibilite' => 0,
|
||||
'avec_note' => 0
|
||||
];
|
||||
|
||||
// Compter les différents critères pour chaque commerce
|
||||
foreach ($commerces as $commerce) {
|
||||
if ($commerce->hasOpeningHours()) {
|
||||
$counters['avec_horaires']++;
|
||||
}
|
||||
if ($commerce->hasAddress()) {
|
||||
$counters['avec_adresse']++;
|
||||
}
|
||||
if ($commerce->hasWebsite()) {
|
||||
$counters['avec_site']++;
|
||||
}
|
||||
if ($commerce->hasWheelchair()) {
|
||||
$counters['avec_accessibilite']++;
|
||||
}
|
||||
if ($commerce->hasNote()) {
|
||||
$counters['avec_note']++;
|
||||
}
|
||||
$commerce->setStats($stats);
|
||||
}
|
||||
|
||||
// Mettre à jour les statistiques
|
||||
$stats->setPlacesCount(count($commerces));
|
||||
$stats->setAvecHoraires($counters['avec_horaires']);
|
||||
$stats->setAvecAdresse($counters['avec_adresse']);
|
||||
$stats->setAvecSite($counters['avec_site']);
|
||||
$stats->setAvecAccessibilite($counters['avec_accessibilite']);
|
||||
$stats->setAvecNote($counters['avec_note']);
|
||||
$stats->computeCompletionPercent();
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$osm_object_ids = [];
|
||||
|
||||
if ($commerces) {
|
||||
if ($commerces) {
|
||||
// Extraire les osm_object_ids des commerces existants
|
||||
$osm_object_ids = array_map(function($commerce) {
|
||||
return $commerce->getOsmId();
|
||||
|
@ -53,7 +156,7 @@ final class AdminController extends AbstractController
|
|||
$results = array_filter($results, function($commerce) use ($osm_object_ids) {
|
||||
return !in_array($commerce['id'], $osm_object_ids);
|
||||
});
|
||||
|
||||
|
||||
// on crée un commerce pour chaque résultat qui reste
|
||||
foreach ($results as $result) {
|
||||
$commerce = new Place();
|
||||
|
@ -65,11 +168,18 @@ final class AdminController extends AbstractController
|
|||
->setUuidForUrl($this->motocultrice->uuid_create())
|
||||
->setOptedOut(false)
|
||||
->setDead(false)
|
||||
->setNote($result['note'] ?? null)
|
||||
|
||||
->setModifiedDate(new \DateTime())
|
||||
->setAskedHumainsSupport(false)
|
||||
->setLastContactAttemptDate(null)
|
||||
->setStats(null);
|
||||
->setStats(null)
|
||||
->setNote($result['tags'] && isset($result['tags']['note']) ? isset($result['tags']['note']) : null)
|
||||
->setHasOpeningHours($result['tags'] && isset($result['tags']['opening_hours']) ? isset($result['tags']['opening_hours']) : null)
|
||||
->setHasAddress(($result['tags'] && isset($result['tags']['address']) || $result['tags'] && isset($result['tags']['contact:address'])) ? isset($result['tags']['address']) : null)
|
||||
->setHasWebsite($result['tags'] && isset($result['tags']['website']) ? $result['tags']['website'] : null)
|
||||
->setHasWheelchair($result['tags'] && isset($result['tags']['wheelchair']) ? $result['tags']['wheelchair'] : null)
|
||||
->setHasNote($result['tags'] && isset($result['tags']['note']) ? $result['tags']['note'] : null)
|
||||
;
|
||||
$this->entityManager->persist($commerce);
|
||||
}
|
||||
|
||||
|
@ -77,7 +187,99 @@ final class AdminController extends AbstractController
|
|||
|
||||
return $this->render('admin/labourage_results.html.twig', [
|
||||
'results' => $results,
|
||||
'commerces' => $commerces,
|
||||
'zone' => $zip_code,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/delete/{id}', name: 'app_admin_delete')]
|
||||
public function delete(int $id): Response
|
||||
{
|
||||
$commerce = $this->entityManager->getRepository(Place::class)->find($id);
|
||||
$name = $commerce->getName();
|
||||
$this->entityManager->remove($commerce);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Le lieu '.$name.' a été supprimé avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
|
||||
|
||||
return $this->redirectToRoute('app_admin_dashboard');
|
||||
}
|
||||
|
||||
#[Route('/admin/delete_by_zone/{zip_code}', name: 'app_admin_delete_by_zone')]
|
||||
public function delete_by_zone(string $zip_code): Response
|
||||
{
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]);
|
||||
|
||||
foreach ($commerces as $commerce) {
|
||||
$this->entityManager->remove($commerce);
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Tous les commerces de la zone '.$zip_code.' ont été supprimés avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
|
||||
|
||||
return $this->redirectToRoute('app_admin_dashboard');
|
||||
}
|
||||
|
||||
|
||||
#[Route('/admin/export', name: 'app_admin_export')]
|
||||
public function export(): Response
|
||||
{
|
||||
$places = $this->entityManager->getRepository(Place::class)->findAll();
|
||||
|
||||
$csvData = [];
|
||||
$csvData[] = [
|
||||
'Nom',
|
||||
'Email',
|
||||
'Code postal',
|
||||
'ID OSM',
|
||||
'Type OSM',
|
||||
'Date de modification',
|
||||
'Date dernier contact',
|
||||
'Note',
|
||||
'Désabonné',
|
||||
'Inactif',
|
||||
'Support humain demandé',
|
||||
'A des horaires',
|
||||
'A une adresse',
|
||||
'A un site web',
|
||||
'A accessibilité',
|
||||
'A une note'
|
||||
];
|
||||
|
||||
foreach ($places as $place) {
|
||||
$csvData[] = [
|
||||
$place->getName(),
|
||||
$place->getEmail(),
|
||||
$place->getZipCode(),
|
||||
$place->getOsmId(),
|
||||
$place->getOsmKind(),
|
||||
$place->getModifiedDate() ? $place->getModifiedDate()->format('Y-m-d H:i:s') : '',
|
||||
$place->getLastContactAttemptDate() ? $place->getLastContactAttemptDate()->format('Y-m-d H:i:s') : '',
|
||||
$place->getNote(),
|
||||
$place->isOptedOut() ? 'Oui' : 'Non',
|
||||
$place->isDead() ? 'Oui' : 'Non',
|
||||
$place->isAskedHumainsSupport() ? 'Oui' : 'Non',
|
||||
$place->hasOpeningHours() ? 'Oui' : 'Non',
|
||||
$place->hasAddress() ? 'Oui' : 'Non',
|
||||
$place->hasWebsite() ? 'Oui' : 'Non',
|
||||
$place->hasWheelchair() ? 'Oui' : 'Non',
|
||||
$place->hasNote() ? 'Oui' : 'Non'
|
||||
];
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('Content-Type', 'text/csv');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="export_places.csv"');
|
||||
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
foreach ($csvData as $row) {
|
||||
fputcsv($handle, $row, ';');
|
||||
}
|
||||
rewind($handle);
|
||||
$response->setContent(stream_get_contents($handle));
|
||||
fclose($handle);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,12 +40,31 @@ class PublicController extends AbstractController
|
|||
{
|
||||
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]);
|
||||
if (!$place) {
|
||||
$this->addFlash('warning', 'Ce lien de modification n\'existe pas.');
|
||||
return $this->redirectToRoute('app_public_index');
|
||||
}
|
||||
$commerce = $this->motocultrice->get_osm_object_data($place->getOsmKind(), $place->getOsmId());
|
||||
|
||||
if ($place->getOsmKind() === 'relation') {
|
||||
$this->addFlash('warning', 'Les objets OSM de type "relation" ne sont pas gérés dans cet outil.');
|
||||
return $this->redirectToRoute('app_public_index');
|
||||
}
|
||||
|
||||
// récupérer les tags de base
|
||||
$base_tags = $this->motocultrice->base_tags;
|
||||
$base_tags = array_fill_keys($base_tags, '');
|
||||
|
||||
$commerce_overpass = $this->motocultrice->get_osm_object_data($place->getOsmKind(), $place->getOsmId());
|
||||
// Fusionner les tags de base avec les tags existants
|
||||
|
||||
$commerce_overpass['tags_converted'] = array_merge($base_tags, $commerce_overpass['tags_converted']);
|
||||
|
||||
// Trier les tags par ordre alphabétique des clés
|
||||
ksort($commerce_overpass['tags_converted']);
|
||||
|
||||
return $this->render('public/edit.html.twig', [
|
||||
'commerce' => $commerce,
|
||||
'commerce_overpass' => $commerce_overpass,
|
||||
'name' => $name,
|
||||
'commerce' => $place,
|
||||
'osm_kind' => $place->getOsmKind(),
|
||||
"mapbox_token" => $_ENV['MAPBOX_TOKEN'],
|
||||
"maptiler_token" => $_ENV['MAPTILER_TOKEN'],
|
||||
|
@ -57,7 +76,8 @@ class PublicController extends AbstractController
|
|||
{
|
||||
// get stats
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findAll();
|
||||
$places = $this->entityManager->getRepository(Place::class)->findAll();
|
||||
|
||||
$places = $this->entityManager->getRepository(Place::class)->findBy([], ['zip_code' => 'ASC', 'name' => 'ASC']);
|
||||
return $this->render('public/dashboard.html.twig', [
|
||||
'controller_name' => 'PublicController',
|
||||
'stats' => $stats,
|
||||
|
@ -108,6 +128,9 @@ class PublicController extends AbstractController
|
|||
// Récupérer le token OSM depuis les variables d'environnement
|
||||
$osm_api_token = $_ENV['APP_OSM_BEARER'];
|
||||
|
||||
|
||||
$exception = false;
|
||||
$exception_message = "";
|
||||
try {
|
||||
$client = new Client();
|
||||
|
||||
|
@ -186,6 +209,8 @@ class PublicController extends AbstractController
|
|||
}
|
||||
} catch (\Exception $e) {
|
||||
$status = "Erreur lors de la communication avec l'API OSM: " . $e->getMessage();
|
||||
$exception = true;
|
||||
$exception_message = $e->getMessage();
|
||||
// Debug de la réponse en cas d'erreur
|
||||
if (method_exists($e, 'getResponse')) {
|
||||
var_dump($e->getResponse()->getBody()->getContents());
|
||||
|
@ -199,6 +224,8 @@ class PublicController extends AbstractController
|
|||
'controller_name' => 'PublicController',
|
||||
'commerce' => $commerce,
|
||||
'status' => $status,
|
||||
'exception' => $exception,
|
||||
'exception_message' => $exception_message,
|
||||
'mapbox_token' => $_ENV['MAPBOX_TOKEN'],
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
|
||||
]);
|
||||
|
|
|
@ -21,7 +21,7 @@ class HistoryFixtures extends Fixture
|
|||
// Créer quelques places de test
|
||||
for ($i = 0; $i < 15; $i++) {
|
||||
$place = new Place();
|
||||
$place->setName($faker->company)
|
||||
$place->setName($faker->company . ' (mock)')
|
||||
->setUuidForUrl($faker->uuid)
|
||||
->setOsmId((string)$faker->numberBetween(1000000, 9999999))
|
||||
->setOsmKind($faker->randomElement(['node', 'way', 'relation']))
|
||||
|
@ -48,7 +48,7 @@ class HistoryFixtures extends Fixture
|
|||
// Créer des statistiques de test
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$stat = new Stats();
|
||||
$stat->setZone($faker->city)
|
||||
$stat->setZone($faker->city . ' (mock)')
|
||||
->setCompletionPercent($faker->numberBetween(0, 100))
|
||||
->addPlace( $faker->randomElement($places_list))
|
||||
->addPlace( $faker->randomElement($places_list))
|
||||
|
|
|
@ -61,6 +61,21 @@ class Place
|
|||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $has_opening_hours = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $has_address = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $has_website = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $has_wheelchair = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $has_note = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->histories = new ArrayCollection();
|
||||
|
@ -256,4 +271,77 @@ class Place
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasOpeningHours(): ?bool
|
||||
{
|
||||
return $this->has_opening_hours;
|
||||
}
|
||||
|
||||
public function setHasOpeningHours(?bool $has_opening_hours): static
|
||||
{
|
||||
$this->has_opening_hours = $has_opening_hours;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAddress(): ?bool
|
||||
{
|
||||
return $this->has_address;
|
||||
}
|
||||
|
||||
public function setHasAddress(?bool $has_address): static
|
||||
{
|
||||
$this->has_address = $has_address;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasWebsite(): ?bool
|
||||
{
|
||||
return $this->has_website;
|
||||
}
|
||||
|
||||
public function setHasWebsite(?bool $has_website): static
|
||||
{
|
||||
$this->has_website = $has_website;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasWheelchair(): ?bool
|
||||
{
|
||||
return $this->has_wheelchair;
|
||||
}
|
||||
|
||||
public function setHasWheelchair(?bool $has_wheelchair): static
|
||||
{
|
||||
$this->has_wheelchair = $has_wheelchair;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasNote(): ?bool
|
||||
{
|
||||
return $this->has_note;
|
||||
}
|
||||
|
||||
public function setHasNote(?bool $has_note): static
|
||||
{
|
||||
$this->has_note = $has_note;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function getPlaceCount(): ?int
|
||||
{
|
||||
return $this->place_count;
|
||||
}
|
||||
|
||||
public function setPlaceCount(int $place_count): static
|
||||
{
|
||||
$this->place_count = $place_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,9 +28,54 @@ class Stats
|
|||
#[ORM\OneToMany(targetEntity: Place::class, mappedBy: 'stats')]
|
||||
private Collection $places;
|
||||
|
||||
#[ORM\Column(type: Types::SMALLINT)]
|
||||
// nombre de commerces dans la zone
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $places_count = null;
|
||||
|
||||
// nombre de commerces avec horaires
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $avec_horaires = null;
|
||||
|
||||
// nombre de commerces avec adresse
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $avec_adresse = null;
|
||||
|
||||
// nombre de commerces avec site
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $avec_site = null;
|
||||
|
||||
// nombre de commerces avec accessibilité
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $avec_accessibilite = null;
|
||||
|
||||
// nombre de commerces avec note
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $avec_note = null;
|
||||
|
||||
// calcule le pourcentage de complétion de la zone
|
||||
public function computeCompletionPercent(): ?int
|
||||
{
|
||||
// Si aucun commerce, on retourne 0
|
||||
if ($this->places_count === 0 || $this->places_count === null) {
|
||||
$this->setCompletionPercent(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// On prend le maximum entre les différents critères
|
||||
$max = max(
|
||||
$this->avec_horaires ?? 0,
|
||||
$this->avec_adresse ?? 0,
|
||||
$this->avec_site ?? 0,
|
||||
$this->avec_accessibilite ?? 0,
|
||||
$this->avec_note ?? 0
|
||||
);
|
||||
$computed = round(($max) / $this->places_count * 100);
|
||||
$this->setCompletionPercent($computed);
|
||||
return $this->completion_percent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->places = new ArrayCollection();
|
||||
|
@ -106,4 +151,66 @@ class Stats
|
|||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
public function getAvecHoraires(): ?int
|
||||
{
|
||||
return $this->avec_horaires;
|
||||
}
|
||||
|
||||
public function setAvecHoraires(int $avec_horaires): static
|
||||
{
|
||||
$this->avec_horaires = $avec_horaires;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecAdresse(): ?int
|
||||
{
|
||||
return $this->avec_adresse;
|
||||
}
|
||||
|
||||
public function setAvecAdresse(int $avec_adresse): static
|
||||
{
|
||||
$this->avec_adresse = $avec_adresse;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecSite(): ?int
|
||||
{
|
||||
return $this->avec_site;
|
||||
}
|
||||
|
||||
public function setAvecSite(int $avec_site): static
|
||||
{
|
||||
$this->avec_site = $avec_site;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecAccessibilite(): ?int
|
||||
{
|
||||
return $this->avec_accessibilite;
|
||||
}
|
||||
|
||||
public function setAvecAccessibilite(int $avec_accessibilite): static
|
||||
{
|
||||
$this->avec_accessibilite = $avec_accessibilite;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecNote(): ?int
|
||||
{
|
||||
return $this->avec_note;
|
||||
}
|
||||
|
||||
public function setAvecNote(int $avec_note): static
|
||||
{
|
||||
$this->avec_note = $avec_note;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -10,12 +10,26 @@ class Motocultrice
|
|||
private $overpassApiUrl = 'https://overpass-api.de/api/interpreter';
|
||||
private $osmApiUrl = 'https://www.openstreetmap.org/api/0.6';
|
||||
|
||||
public $base_tags = [
|
||||
'name',
|
||||
'opening_hours',
|
||||
'contact:email',
|
||||
'contact:phone',
|
||||
'wheelchair',
|
||||
'addr:housenumber',
|
||||
'addr:street',
|
||||
'contact:website',
|
||||
'contact:mastodon',
|
||||
// 'EEEEEEEEEEEEEEEEEEE'
|
||||
];
|
||||
private $more_tags = ['image', 'ref:FR:SIRET'];
|
||||
public function __construct(
|
||||
private HttpClientInterface $client,
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function labourer(string $zone): array
|
||||
{
|
||||
if (!$zone) {
|
||||
|
@ -58,6 +72,9 @@ QUERY;
|
|||
if (isset($data['elements'])) {
|
||||
foreach ($data['elements'] as $element) {
|
||||
if (isset($element['tags'])) {
|
||||
|
||||
|
||||
|
||||
$email = $element['tags']['contact:email'] ?? $element['tags']['email'] ?? null;
|
||||
// On passe si pas d'email
|
||||
if (!$email) {
|
||||
|
@ -86,6 +103,8 @@ QUERY;
|
|||
public function get_osm_object_data($osm_kind = 'node', $osm_object_id = 12855459190)
|
||||
{
|
||||
$object_id = "https://www.openstreetmap.org/api/0.6/".$osm_kind."/".$osm_object_id;
|
||||
// dump($object_id);
|
||||
// die();
|
||||
|
||||
try {
|
||||
$response = $this->client->request('GET', $object_id);
|
||||
|
@ -96,8 +115,8 @@ QUERY;
|
|||
throw new \Exception("Impossible de récupérer les données OSM : " . $e->getMessage());
|
||||
}
|
||||
|
||||
// convertir les tags en clés et valeurs
|
||||
$osm_object_data['tags_converted'] = [];
|
||||
// convertir les tags en clés et valeurs, remplir avec les tags de base
|
||||
$osm_object_data['tags_converted'] = $this->base_tags;
|
||||
// Initialiser le tableau des tags convertis
|
||||
if (isset($osm_object_data['node'])) {
|
||||
$osm_object_data['node']['tags_converted'] = [];
|
||||
|
@ -236,4 +255,48 @@ QUERY;
|
|||
throw new \Exception("Erreur lors de la communication avec l'API OSM : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function calculateStats(array $places): array
|
||||
{
|
||||
$counters = [
|
||||
'avec_horaires' => 0,
|
||||
'avec_adresse' => 0,
|
||||
'avec_site' => 0,
|
||||
'avec_accessibilite' => 0,
|
||||
'avec_note' => 0
|
||||
];
|
||||
|
||||
foreach ($places as $place) {
|
||||
if ($place->hasOpeningHours()) {
|
||||
$counters['avec_horaires']++;
|
||||
}
|
||||
if ($place->hasAddress()) {
|
||||
$counters['avec_adresse']++;
|
||||
}
|
||||
if ($place->hasWebsite()) {
|
||||
$counters['avec_site']++;
|
||||
}
|
||||
if ($place->hasWheelchair()) {
|
||||
$counters['avec_accessibilite']++;
|
||||
}
|
||||
if ($place->hasNote()) {
|
||||
$counters['avec_note']++;
|
||||
}
|
||||
}
|
||||
|
||||
$totalPlaces = count($places);
|
||||
$completionPercent = 0;
|
||||
|
||||
if ($totalPlaces > 0) {
|
||||
$totalCriteria = 5; // nombre total de critères
|
||||
$totalCompleted = array_sum($counters);
|
||||
$completionPercent = round(($totalCompleted / ($totalPlaces * $totalCriteria)) * 100);
|
||||
}
|
||||
|
||||
return [
|
||||
'places_count' => $totalPlaces,
|
||||
'completion_percent' => $completionPercent,
|
||||
'counters' => $counters
|
||||
];
|
||||
}
|
||||
}
|
|
@ -11,15 +11,37 @@
|
|||
<div class="example-wrapper">
|
||||
<h1>Labourage fait sur la zone "{{ zone }}" ✅</h1>
|
||||
|
||||
<p>
|
||||
lieux trouvés en plus: {{ results|length }}
|
||||
</p>
|
||||
|
||||
{# {{ dump(results) }} #}
|
||||
{% for commerce in results %}
|
||||
<p>
|
||||
{{ commerce.name }}
|
||||
</p>
|
||||
<pre>
|
||||
{{ dump(commerce) }}
|
||||
</pre>
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
commerces existants disposant d'un moyen de contact mail: {{ commerces|length }}
|
||||
</p>
|
||||
{# {{ dump(commerces[0]) }} #}
|
||||
<ul>
|
||||
{% for commerce in commerces %}
|
||||
<li>
|
||||
{% if commerce.name is not null %}
|
||||
{{ commerce.name }},
|
||||
{% else %}
|
||||
un lieu sans nom
|
||||
{% endif %}
|
||||
|
||||
{# {{ commerce.note }},
|
||||
{{ commerce.has_address }},
|
||||
{{ commerce.has_website }},
|
||||
{{ commerce.has_wheelchair }},
|
||||
{{ commerce.has_note }} #}
|
||||
|
||||
<a href="https://www.openstreetmap.org/{{ commerce.getOsmKind() }}/{{ commerce.getOsmId() }}" target="_blank">voir sur osm</a>
|
||||
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
192
templates/admin/stats.html.twig
Normal file
192
templates/admin/stats.html.twig
Normal file
|
@ -0,0 +1,192 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}{{ 'display.stats'|trans }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="card mt-4 p-4">
|
||||
<h1 class="card-title">{{ 'display.stats'|trans }}</h1>
|
||||
<p>
|
||||
{{ stats.zone }}
|
||||
</p>
|
||||
<p>
|
||||
{# {{ dump(stats) }} #}
|
||||
</p>
|
||||
{{ stats.getCompletionPercent() }} % complété sur les critères donnés.
|
||||
<br>
|
||||
{{ stats.getPlacesCount() }} commerces dans la zone.
|
||||
<br>
|
||||
{{ stats.getAvecHoraires() }} commerces avec horaires.
|
||||
<br>
|
||||
{{ stats.getAvecAdresse() }} commerces avec adresse.
|
||||
<br>
|
||||
{{ stats.getAvecSite() }} commerces avec site web renseigné.
|
||||
<br>
|
||||
{{ stats.getAvecAccessibilite() }} commerces avec accessibilité renseignée.
|
||||
<br>
|
||||
{{ stats.getAvecNote() }} commerces avec note renseignée.
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<h1 class="card-title">Tableau des commerces</h1>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom ({{ stats.getPlacesCount() }})</th>
|
||||
<th>Adresse ({{ stats.getAvecAdresse() }} / {{ stats.getPlacesCount() }})</th>
|
||||
<th>Site web ({{ stats.getAvecSite() }} / {{ stats.getPlacesCount() }})</th>
|
||||
<th>Accessibilité ({{ stats.getAvecAccessibilite() }} / {{ stats.getPlacesCount() }})</th>
|
||||
<th>Note ({{ stats.getAvecNote() }} / {{ stats.getPlacesCount() }})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for commerce in stats.places %}
|
||||
<tr>
|
||||
<td style="background-color: {{ commerce.hasAddress() ? 'yellowgreen' : 'transparent' }};">{{ commerce.name }}</td>
|
||||
<td style="background-color: {{ commerce.hasAddress() ? 'yellowgreen' : 'transparent' }};">{{ commerce.address }}</td>
|
||||
<td style="background-color: {{ commerce.hasWebsite() ? 'yellowgreen' : 'transparent' }};">{{ commerce.website }}</td>
|
||||
<td style="background-color: {{ commerce.hasWheelchair() ? 'yellowgreen' : 'transparent' }};">{{ commerce.wheelchair }}</td>
|
||||
<td style="background-color: {{ commerce.hasNote() ? 'yellowgreen' : 'transparent' }};">{{ commerce.note }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Styles spécifiques pour la page stats */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--light-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.modifications-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.modification-item {
|
||||
padding: 0.75rem;
|
||||
background-color: var(--light-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modification-header {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.modification-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.modification-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modification-type {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.shop-types-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shop-type-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--light-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.shop-type-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shop-type-count {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.places-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.places-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.places-table th,
|
||||
.places-table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.places-table th {
|
||||
background-color: var(--light-gray);
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.places-table tr:hover {
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.places-table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
|
@ -10,6 +10,7 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
|
||||
{% block stylesheets %}
|
||||
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -17,6 +18,7 @@
|
|||
<body>
|
||||
<header class="main-header">
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="{{ path('app_public_index') }}">
|
||||
|
@ -24,6 +26,38 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ label }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ path('app_public_index') }}">{{ 'display.home'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_public_dashboard') }}">{{ 'display.stats'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://openstreetmap.fr/contact/">{{ 'display.contact_humans'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="body-landing">
|
||||
|
@ -34,7 +68,9 @@
|
|||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>OpenStreetMap Mon Commerce</p>
|
||||
<p>Licence AGPLv3+, fait par CipherBliss, membre de la fédération des professionels d'OpenStreetMap, Sources des données : <a href="https://www.openstreetmap.org/">OpenStreetMap</a>.
|
||||
<p>Licence AGPLv3+,
|
||||
fait par Tykayn de
|
||||
<a href="https://www.cipherbliss.com">CipherBliss EI</a>, membre de la fédération des professionels d'OpenStreetMap, Sources des données : <a href="https://www.openstreetmap.org/">OpenStreetMap</a>.
|
||||
<br> <a href="https://www.openstreetmap.org/copyright">OpenStreetMap France</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,30 +18,7 @@
|
|||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ path('app_public_index') }}">{{ 'display.home'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_public_dashboard') }}">{{ 'display.stats'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://openstreetmap.fr/contact/">{{ 'display.contact_humans'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
@ -50,22 +27,18 @@
|
|||
<h1 class="card-title mb-4">{{ 'display.welcome'|trans }}</h1>
|
||||
<div id="map" style="height: 400px; width: 100%;" class="rounded"></div>
|
||||
|
||||
{% if commerce is not empty %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if commerce_overpass is not empty %}
|
||||
|
||||
|
||||
<form action="{{ path('app_public_submit', {'osm_object_id': commerce['@attributes'].id, 'version': commerce['@attributes'].version, 'changesetID': commerce['@attributes'].changeset }) }}" method="post" class="needs-validation">
|
||||
<form action="{{ path('app_public_submit', {'osm_object_id': commerce_overpass['@attributes'].id, 'version': commerce_overpass['@attributes'].version, 'changesetID': commerce_overpass['@attributes'].changeset }) }}" method="post" class="needs-validation">
|
||||
<input type="hidden" name="osm_kind" value="{{ osm_kind }}">
|
||||
<div class="mb-3">
|
||||
<label for="commerce_id" class="form-label">{{ 'display.modify_commerce'|trans }}:
|
||||
<strong>
|
||||
{% if commerce.tags_converted.name is defined %}
|
||||
{{ commerce.tags_converted.name }}
|
||||
{% elseif commerce['@attributes'].name is defined %}
|
||||
{{ commerce['@attributes'].name }}
|
||||
{% if commerce_overpass.tags_converted.name is defined %}
|
||||
{{ commerce_overpass.tags_converted.name }}
|
||||
{% elseif commerce_overpass['@attributes'].name is defined %}
|
||||
{{ commerce_overpass['@attributes'].name }}
|
||||
{% else %}
|
||||
{{ 'display.unknown'|trans }}
|
||||
{% endif %}
|
||||
|
@ -98,7 +71,7 @@
|
|||
{# cas d'une mairie
|
||||
#}
|
||||
|
||||
{% if commerce.tags_converted.amenity is defined %}
|
||||
{# {% if commerce.tags_converted.amenity is defined %}
|
||||
{% if commerce.tags_converted.amenity == 'townhall' %}
|
||||
{% include 'public/edit/townhall.html.twig' %}
|
||||
{% endif %}
|
||||
|
@ -106,16 +79,16 @@
|
|||
{% if commerce.tags_converted.amenity == 'restaurant' %}
|
||||
{% include 'public/edit/restaurant.html.twig' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %} #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% include 'public/edit/ask_angela.html.twig' %}
|
||||
{% include 'public/edit/wheelchair.html.twig' %}
|
||||
{% include 'public/edit/opening_hours.html.twig' %}
|
||||
{% include 'public/edit/address.html.twig' %}
|
||||
{% include 'public/edit/tags.html.twig' %}
|
||||
{# {# {% include 'public/edit/wheelchair.html.twig' %} #}
|
||||
{# {% include 'public/edit/opening_hours.html.twig' %}
|
||||
{% include 'public/edit/address.html.twig' %} #} #}
|
||||
{% include 'public/edit/tags.html.twig' %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ 'display.submit'|trans }}</button>
|
||||
</form>
|
||||
|
@ -124,15 +97,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<span class="p-3">
|
||||
<span class="last-modification">{{ 'display.last_modification'|trans }}: {{ commerce['@attributes'].timestamp }}</span>,
|
||||
{{ 'display.days_ago'|trans({'%days%': date(commerce['@attributes'].timestamp).diff(date()).days}) }}
|
||||
<span class="last-modification">{{ 'display.last_modification'|trans }}: {{ commerce_overpass['@attributes'].timestamp }}</span>,
|
||||
{{ 'display.days_ago'|trans({'%days%': date(commerce_overpass['@attributes'].timestamp).diff(date()).days}) }}
|
||||
{{ 'display.by'|trans }}
|
||||
<a href="https://www.openstreetmap.org/user/{{ commerce['@attributes'].user }}" target="_blank">{{ commerce['@attributes'].user }}</a>
|
||||
<a href="https://www.openstreetmap.org/user/{{ commerce_overpass['@attributes'].user }}" target="_blank">{{ commerce_overpass['@attributes'].user }}</a>
|
||||
<div class="lien-OpenStreetMap">
|
||||
<a href="https://www.openstreetmap.org/node/{{ commerce['@attributes'].id }}" target="_blank">{{ 'display.view_on_osm'|trans }}</a>
|
||||
<a href="https://www.openstreetmap.org/node/{{ commerce_overpass['@attributes'].id }}" target="_blank">{{ 'display.view_on_osm'|trans }}</a>
|
||||
</div>
|
||||
|
||||
{{ dump(commerce) }}
|
||||
{{ dump(commerce_overpass) }}
|
||||
</span>
|
||||
|
||||
<div class="disclaimer p-3">
|
||||
|
@ -162,25 +135,31 @@
|
|||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js'></script>
|
||||
{# <script>
|
||||
<script>
|
||||
{% if commerce is not empty and mapbox_token is not empty and maptiler_token is not empty %}
|
||||
mapboxgl.accessToken = '{{ mapbox_token }}';
|
||||
map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'https://api.maptiler.com/maps/basic-v2/style.json?key={{ maptiler_token }}',
|
||||
center: [{{ commerce['@attributes'].lon }}, {{ commerce['@attributes'].lat }}],
|
||||
center: [
|
||||
{# {{ commerce_overpass['@attributes'].lon }}, #}
|
||||
{# {{ commerce_overpass['@attributes'].lat }} #}
|
||||
],
|
||||
|
||||
zoom: 14
|
||||
});
|
||||
|
||||
// Ajout du marqueur
|
||||
new mapboxgl.Marker()
|
||||
.setLngLat([{{ commerce.lon }}, {{ commerce.lat }}])
|
||||
.setLngLat([
|
||||
{# {{ commerce.lon }}, #}
|
||||
{# {{ commerce.lat }} #}
|
||||
])
|
||||
.setPopup(new mapboxgl.Popup({
|
||||
offset: 25
|
||||
}).setHTML('<h1>{{ commerce.tags_converted.name }}</h1>'))
|
||||
}).setHTML('<h1>{{ commerce_overpass.tags_converted.name }}</h1>'))
|
||||
.addTo(map);
|
||||
{% endif %}
|
||||
</script> #}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,7 +2,16 @@
|
|||
<h2>{{ 'display.opening_hours'|trans }}</h2>
|
||||
<p class="description">{{ 'display.opening_hours_description'|trans }}</p>
|
||||
|
||||
<input type="checkbox" name="commerce_tag_value__opening_hours_1" value="yes">
|
||||
{% if commerce_overpass.tags_converted.opening_hours is defined %}
|
||||
{{ dump(commerce_overpass.tags_converted.opening_hours) }}
|
||||
{% else %}
|
||||
<input type="text" name="commerce_tag_value__opening_hours" value="">
|
||||
<br> ajoutez les horaires au format OSM
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
{# <input type="checkbox" name="commerce_tag_value__opening_hours_1" value="yes">
|
||||
Lundi de
|
||||
|
||||
<input type="number" name="commerce_tag_value__opening_hours_1_midday_hour" value="">
|
||||
|
@ -16,7 +25,8 @@
|
|||
à
|
||||
<input type="number" name="commerce_tag_value__opening_hours_1_midday_minute" value="">.
|
||||
|
||||
<input type="checkbox" name="commerce_tag_value__opening_hours_1_evening" value="yes">
|
||||
<input type="checkbox" name="commerce_tag_value__opening_hours_1_evening" value="yes"> #}
|
||||
<hr>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/yohours@0.0.14/src/index.min.js"></script>
|
||||
</script>
|
||||
</div>
|
|
@ -1,21 +1,16 @@
|
|||
{% block tags %}<fieldset>
|
||||
<div id="tags">
|
||||
|
||||
{% for attributes in commerce.tag %}
|
||||
{% for kv in attributes %}
|
||||
{# {% if kv.k == 'opening_hours' %}
|
||||
{{ 'display.keys.opening_hours'|trans }}
|
||||
{% else %} #}
|
||||
{% for k, v in commerce_overpass.tags_converted %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control hidden" name="commerce_tag_key__{{ kv.k }}" value="{{ kv.k }}" readonly>
|
||||
<span class="label-translated">{{ ('display.keys.' ~ kv.k)|trans }}</span>
|
||||
<input type="text" class="form-control hidden" name="commerce_tag_key__{{ k }}" value="{{ k }}" readonly>
|
||||
<span class="label-translated">{{ ('display.keys.' ~ k)|trans }}</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control" name="commerce_tag_value__{{ kv.k }}" value="{{ kv.v }}">
|
||||
<input type="text" class="form-control" name="commerce_tag_value__{{ k }}" value="{{ v }}">
|
||||
</div>
|
||||
</div>
|
||||
{# {% endif %} #}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
</div></fieldset>
|
||||
|
|
|
@ -14,40 +14,12 @@
|
|||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ path('app_public_index') }}">{{ 'display.home'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_public_dashboard') }}">{{ 'display.stats'|trans }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://openstreetmap.fr/contact/">{{ 'display.contact_humans'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title mb-4">{{ 'display.welcome'|trans }}</h1>
|
||||
<div id="map" style="height: 400px; width: 100%;" class="rounded"></div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="card shadow-sm p-4">
|
||||
Ce site permet aux commerçants et aux lieux référencés sur OpenStreetMap de modifier leurs informations facilement pour gagner en visibilité sur des milliers de sites web à la fois.
|
||||
</div>
|
||||
</div>
|
||||
<span class="p-3">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue