301 lines
12 KiB
PHP
301 lines
12 KiB
PHP
<?php
|
|
// search-map.php — Ricerca saloni con MAPPA in cima (frontend portale)
|
|
// Geolocalizzazione browser -> centra e ordina per vicinanza. Fallback: Milano centro.
|
|
// Distanze calcolate lato JavaScript. Nessuna modifica al database.
|
|
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
|
|
|
// --- Parametri ricerca ---
|
|
$q_city = trim($_GET['city'] ?? '');
|
|
$q_service = trim($_GET['service'] ?? '');
|
|
|
|
// --- Query saloni attivi (solo con coordinate: servono per la mappa) ---
|
|
$where = ["sh.active = 1", "sh.latitude IS NOT NULL", "sh.longitude IS NOT NULL"];
|
|
$params = [];
|
|
|
|
if ($q_city !== '') {
|
|
$where[] = "sh.city = ?";
|
|
$params[] = $q_city;
|
|
}
|
|
if ($q_service !== '') {
|
|
$where[] = "EXISTS (SELECT 1 FROM services sv WHERE sv.shop_id = sh.id AND sv.is_active = 1 AND sv.name = ?)";
|
|
$params[] = $q_service;
|
|
}
|
|
$whereSql = implode(' AND ', $where);
|
|
|
|
$sql = "
|
|
SELECT sh.id, sh.name, sh.slug, sh.description, sh.address, sh.city, sh.province,
|
|
sh.phone, sh.latitude, sh.longitude, sh.logo, sh.cover_image,
|
|
(SELECT MIN(sv.price) FROM services sv WHERE sv.shop_id = sh.id AND sv.is_active = 1) AS min_price
|
|
FROM shops sh
|
|
WHERE $whereSql
|
|
ORDER BY sh.name ASC
|
|
";
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$salons = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$totalSalons = count($salons);
|
|
|
|
// Città e servizi per i filtri della booking-bar
|
|
$cities = $pdo->query("
|
|
SELECT DISTINCT city FROM shops
|
|
WHERE active = 1 AND city IS NOT NULL AND city <> ''
|
|
ORDER BY city ASC
|
|
")->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
$allServices = $pdo->query("
|
|
SELECT DISTINCT name FROM services
|
|
WHERE is_active = 1 AND name IS NOT NULL AND name <> ''
|
|
ORDER BY name ASC
|
|
")->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
// Preferiti dell'utente loggato
|
|
$favIds = [];
|
|
if (!empty($_SESSION['iduserlogin'])) {
|
|
$fstmt = $pdo->prepare("SELECT shop_id FROM user_favorite_shops WHERE user_id = ?");
|
|
$fstmt->execute([$_SESSION['iduserlogin']]);
|
|
$favIds = $fstmt->fetchAll(PDO::FETCH_COLUMN);
|
|
}
|
|
|
|
// Dati saloni per il JavaScript (mappa + distanze)
|
|
$salonsJs = array_map(function ($s) {
|
|
return [
|
|
'id' => (int)$s['id'],
|
|
'name' => $s['name'],
|
|
'slug' => $s['slug'],
|
|
'city' => $s['city'],
|
|
'address'=> $s['address'],
|
|
'lat' => (float)$s['latitude'],
|
|
'lng' => (float)$s['longitude'],
|
|
'logo' => $s['logo'],
|
|
'price' => $s['min_price'] !== null ? (float)$s['min_price'] : null,
|
|
];
|
|
}, $salons);
|
|
?>
|
|
<!doctype html>
|
|
<html class="no-js" lang="it">
|
|
|
|
<head>
|
|
<?php include('tophead.php'); ?>
|
|
<?php include('cssinclude.php'); ?>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<style>
|
|
#salonsMap { height: 460px; width: 100%; border-radius: 12px; z-index: 1; }
|
|
.map-toolbar { gap: 10px; }
|
|
.salon-mini-card {
|
|
border: 1px solid #eee; border-radius: 12px; overflow: hidden;
|
|
transition: box-shadow .15s ease, transform .15s ease; background: #fff; cursor: pointer;
|
|
}
|
|
.salon-mini-card:hover, .salon-mini-card.active {
|
|
box-shadow: 0 6px 20px rgba(0,0,0,.12); transform: translateY(-2px);
|
|
}
|
|
.salon-mini-card .thumb { height: 130px; background: #f2f3f7 center/cover no-repeat; }
|
|
.salon-mini-card .body { padding: 12px 14px; }
|
|
.salon-dist { font-size: .8rem; color: #0d6efd; font-weight: 600; }
|
|
.locate-btn { cursor: pointer; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<?php include('preloader.php'); ?>
|
|
|
|
<div class="back-to-top-wrapper">
|
|
<button id="back_to_top" type="button" class="back-to-top-btn">
|
|
<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M11 6L6 1L1 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<?php include('offcanvas.php'); ?>
|
|
|
|
<header class="tp-header-height">
|
|
<?php include('topbar.php'); ?>
|
|
</header>
|
|
|
|
<main>
|
|
|
|
<div class="container-fluid pt-30 pb-30">
|
|
|
|
<!-- Barra di ricerca -->
|
|
<div class="text-center mb-25">
|
|
<h2 class="mb-20">Trova il tuo parrucchiere</h2>
|
|
<?php include('booking-bar.php'); ?>
|
|
</div>
|
|
|
|
<!-- Toolbar mappa -->
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap map-toolbar mb-15">
|
|
<div>
|
|
<span class="fw-600"><?= $totalSalons ?> salon<?= $totalSalons === 1 ? 'e' : 'i' ?></span>
|
|
<?php if ($q_city !== ''): ?><span class="text-muted">a <?= htmlspecialchars($q_city) ?></span><?php endif; ?>
|
|
</div>
|
|
<button type="button" class="tp-btn-sm locate-btn" id="locateBtn">
|
|
<i class="fa-solid fa-location-crosshairs"></i> Usa la mia posizione
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mappa -->
|
|
<div id="salonsMap" class="mb-30"></div>
|
|
|
|
<!-- Lista saloni -->
|
|
<div class="row g-3" id="salonList">
|
|
<?php if (empty($salons)): ?>
|
|
<div class="col-12 text-center py-5">
|
|
<h4>Nessun salone trovato</h4>
|
|
<p class="text-muted">Prova a cambiare città o servizio.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($salons as $s):
|
|
$cover = !empty($s['cover_image']) ? htmlspecialchars($s['cover_image']) : 'assets/img/tour/list/01.jpg';
|
|
?>
|
|
<div class="col-sm-6 col-lg-4 col-xl-3 salon-col" data-id="<?= (int)$s['id'] ?>">
|
|
<div class="salon-mini-card h-100" data-id="<?= (int)$s['id'] ?>">
|
|
<a href="salon.php?slug=<?= urlencode($s['slug']) ?>" class="d-block">
|
|
<div class="thumb" style="background-image:url('<?= $cover ?>')"></div>
|
|
</a>
|
|
<div class="body">
|
|
<div class="fw-700 mb-1"><?= htmlspecialchars($s['name']) ?></div>
|
|
<div class="text-muted small mb-1">
|
|
<i class="fa-solid fa-location-dot"></i>
|
|
<?= htmlspecialchars(trim(($s['address'] ? $s['address'] . ', ' : '') . $s['city'])) ?>
|
|
</div>
|
|
<div class="salon-dist" data-dist-for="<?= (int)$s['id'] ?>"></div>
|
|
<div class="d-flex justify-content-between align-items-center mt-2">
|
|
<?php if ($s['min_price'] !== null): ?>
|
|
<span class="fw-700"><?= number_format((float)$s['min_price'], 2, ',', '.') ?> €</span>
|
|
<?php else: ?><span></span><?php endif; ?>
|
|
<a href="salon.php?slug=<?= urlencode($s['slug']) ?>" class="tp-btn-sm">Prenota</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
<?php include('footer.php'); ?>
|
|
</footer>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<script>
|
|
var SALONS = <?= json_encode($salonsJs, JSON_UNESCAPED_UNICODE) ?>;
|
|
var DEFAULT_CENTER = [45.4642, 9.1900]; // Milano centro (fallback)
|
|
|
|
var map = L.map('salonsMap').setView(DEFAULT_CENTER, 12);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap'
|
|
}).addTo(map);
|
|
|
|
var markers = {};
|
|
var bounds = [];
|
|
|
|
SALONS.forEach(function (s) {
|
|
var m = L.marker([s.lat, s.lng]).addTo(map);
|
|
var priceTxt = s.price !== null ? ('<br>Da ' + s.price.toFixed(2).replace('.', ',') + ' €') : '';
|
|
m.bindPopup('<strong>' + s.name + '</strong><br>' + (s.address ? s.address + ', ' : '') + (s.city || '') + priceTxt +
|
|
'<br><a href="salon.php?slug=' + encodeURIComponent(s.slug) + '">Vedi salone</a>');
|
|
m.on('click', function () { highlightCard(s.id); });
|
|
markers[s.id] = m;
|
|
bounds.push([s.lat, s.lng]);
|
|
});
|
|
|
|
// Adatta la vista a tutti i pin (se ce ne sono)
|
|
if (bounds.length > 0) {
|
|
map.fitBounds(bounds, { padding: [40, 40], maxZoom: 14 });
|
|
}
|
|
|
|
// Distanza Haversine (km)
|
|
function distanceKm(lat1, lon1, lat2, lon2) {
|
|
var R = 6371;
|
|
var dLat = (lat2 - lat1) * Math.PI / 180;
|
|
var dLon = (lon2 - lon1) * Math.PI / 180;
|
|
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) *
|
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
}
|
|
|
|
// Ordina la lista per vicinanza a (lat,lng) e mostra le distanze
|
|
function sortByProximity(lat, lng) {
|
|
var list = document.getElementById('salonList');
|
|
var cols = Array.prototype.slice.call(list.querySelectorAll('.salon-col'));
|
|
|
|
var withDist = SALONS.map(function (s) {
|
|
return { id: s.id, d: distanceKm(lat, lng, s.lat, s.lng) };
|
|
});
|
|
withDist.sort(function (a, b) { return a.d - b.d; });
|
|
|
|
// Aggiorna testo distanza
|
|
withDist.forEach(function (item) {
|
|
var el = list.querySelector('[data-dist-for="' + item.id + '"]');
|
|
if (el) el.textContent = item.d < 1
|
|
? Math.round(item.d * 1000) + ' m da te'
|
|
: item.d.toFixed(1).replace('.', ',') + ' km da te';
|
|
});
|
|
|
|
// Riordina le colonne nel DOM
|
|
withDist.forEach(function (item) {
|
|
var col = list.querySelector('.salon-col[data-id="' + item.id + '"]');
|
|
if (col) list.appendChild(col);
|
|
});
|
|
}
|
|
|
|
function highlightCard(id) {
|
|
document.querySelectorAll('.salon-mini-card').forEach(function (c) {
|
|
c.classList.toggle('active', String(c.dataset.id) === String(id));
|
|
});
|
|
var col = document.querySelector('.salon-col[data-id="' + id + '"]');
|
|
if (col) col.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
// Click su card -> apre popup del pin
|
|
document.querySelectorAll('.salon-mini-card').forEach(function (card) {
|
|
card.addEventListener('click', function (e) {
|
|
if (e.target.closest('a')) return; // lascia passare i link
|
|
var id = this.dataset.id;
|
|
if (markers[id]) { markers[id].openPopup(); map.panTo(markers[id].getLatLng()); }
|
|
highlightCard(id);
|
|
});
|
|
});
|
|
|
|
// Geolocalizzazione
|
|
var userMarker = null;
|
|
function locateUser() {
|
|
if (!navigator.geolocation) { alert('Geolocalizzazione non supportata dal browser.'); return; }
|
|
navigator.geolocation.getCurrentPosition(function (pos) {
|
|
var lat = pos.coords.latitude, lng = pos.coords.longitude;
|
|
map.setView([lat, lng], 13);
|
|
if (userMarker) map.removeLayer(userMarker);
|
|
userMarker = L.circleMarker([lat, lng], {
|
|
radius: 9, color: '#0d6efd', fillColor: '#0d6efd', fillOpacity: .7
|
|
}).addTo(map).bindPopup('Sei qui').openPopup();
|
|
sortByProximity(lat, lng);
|
|
}, function () {
|
|
// permesso negato o errore: resta su default, nessun ordinamento
|
|
}, { enableHighAccuracy: true, timeout: 8000 });
|
|
}
|
|
|
|
document.getElementById('locateBtn').addEventListener('click', locateUser);
|
|
|
|
// Prova a geolocalizzare all'apertura (l'utente deciderà sul prompt)
|
|
locateUser();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|