Files
comelifacciamo/public/search-map.php
T

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: '&copy; 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>