diff --git a/blueprints/admin/routes_audits.py b/blueprints/admin/routes_audits.py index 1869ca1..530fc1c 100644 --- a/blueprints/admin/routes_audits.py +++ b/blueprints/admin/routes_audits.py @@ -1059,6 +1059,143 @@ def admin_gbp_audit_batch_discard(): }) +# ============================================================ +# GBP PLACE ID MATCHING (manual review) +# ============================================================ + +@bp.route('/gbp-audit/match-places') +@login_required +@role_required(SystemRole.ADMIN) +def admin_gbp_match_places(): + """Show companies without google_place_id for manual matching.""" + if not is_audit_owner(): + abort(404) + + db = SessionLocal() + try: + # Get companies without place_id + subq = db.query(CompanyWebsiteAnalysis.company_id).filter( + CompanyWebsiteAnalysis.google_place_id.isnot(None) + ).subquery() + + companies = db.query( + Company.id, Company.name, Company.address_city, Company.website + ).filter( + Company.status == 'active', + ~Company.id.in_(subq) + ).order_by(Company.name).all() + + return render_template('admin/gbp_match_places.html', companies=companies) + finally: + db.close() + + +@bp.route('/gbp-audit/search-place', methods=['POST']) +@login_required +@role_required(SystemRole.ADMIN) +def admin_gbp_search_place(): + """Search Google Places for a company (no name filter — raw results).""" + if not is_audit_owner(): + return jsonify({'error': 'Brak uprawnień'}), 403 + + company_id = request.form.get('company_id', type=int) + if not company_id: + return jsonify({'error': 'Brak company_id'}), 400 + + db = SessionLocal() + try: + company = db.get(Company, company_id) + if not company: + return jsonify({'error': 'Firma nie znaleziona'}), 404 + + try: + from google_places_service import GooglePlacesService + places_service = GooglePlacesService() + except (ImportError, ValueError) as e: + return jsonify({'error': f'Places API niedostępne: {e}'}), 500 + + city = company.address_city or 'Wejherowo' + query = f'{company.name} {city}' + location_bias = {'latitude': 54.6059, 'longitude': 18.2350, 'radius': 50000.0} + + # Search via raw API call — return ALL results for manual review + results = places_service.search_places_raw(query, location_bias=location_bias) + + if not results: + # Try broader search with just company name + results = places_service.search_places_raw(company.name, location_bias=location_bias) + + if not results: + return jsonify({'results': [], 'query': query}) + + places = [] + for p in results: + place_id = p.get('id', '') + if place_id.startswith('places/'): + place_id = place_id.replace('places/', '') + places.append({ + 'place_id': place_id, + 'name': p.get('displayName', {}).get('text', ''), + 'address': p.get('formattedAddress', ''), + 'types': ', '.join((p.get('types') or [])[:3]), + 'rating': p.get('rating'), + 'reviews_count': p.get('userRatingCount'), + }) + + return jsonify({'results': places, 'query': query}) + finally: + db.close() + + +@bp.route('/gbp-audit/confirm-place', methods=['POST']) +@login_required +@role_required(SystemRole.ADMIN) +def admin_gbp_confirm_place(): + """Save confirmed google_place_id for a company.""" + if not is_audit_owner(): + return jsonify({'error': 'Brak uprawnień'}), 403 + + company_id = request.form.get('company_id', type=int) + place_id = request.form.get('place_id', '').strip() + google_name = request.form.get('google_name', '').strip() + + if not company_id or not place_id: + return jsonify({'error': 'Brak company_id lub place_id'}), 400 + + db = SessionLocal() + try: + company = db.get(Company, company_id) + if not company: + return jsonify({'error': 'Firma nie znaleziona'}), 404 + + analysis = db.query(CompanyWebsiteAnalysis).filter( + CompanyWebsiteAnalysis.company_id == company_id + ).first() + + if not analysis: + analysis = CompanyWebsiteAnalysis( + company_id=company_id, + url=company.website, + analyzed_at=datetime.now() + ) + db.add(analysis) + + analysis.google_place_id = place_id + if google_name: + analysis.google_name = google_name + analysis.analyzed_at = datetime.now() + db.commit() + + logger.info(f"Place ID confirmed for company {company_id} ({company.name}): {place_id}") + return jsonify({'status': 'ok', 'message': f'Place ID zapisany dla {company.name}'}) + except Exception as e: + db.rollback() + logger.error(f"Failed to confirm place_id for company {company_id}: {e}") + return jsonify({'error': str(e)[:100]}), 500 + finally: + db.close() + + # ============================================================ # DIGITAL MATURITY DASHBOARD # ============================================================ diff --git a/google_places_service.py b/google_places_service.py index 72fa6c1..97a263f 100644 --- a/google_places_service.py +++ b/google_places_service.py @@ -280,6 +280,41 @@ class GooglePlacesService: logger.error(f"Places search error for '{query}': {e}") return None + def search_places_raw(self, query: str, location_bias: Dict = None) -> List[Dict[str, Any]]: + """ + Search for places and return ALL results (no name filtering). + Used for manual review/matching in admin panel. + """ + body = { + "textQuery": query, + "languageCode": "pl", + "maxResultCount": 5 + } + if location_bias: + body["locationBias"] = { + "circle": { + "center": { + "latitude": location_bias["latitude"], + "longitude": location_bias["longitude"] + }, + "radius": location_bias.get("radius", 5000.0) + } + } + + field_mask = ','.join(f'places.{f}' for f in [ + 'id', 'displayName', 'formattedAddress', 'types', + 'rating', 'userRatingCount', 'googleMapsUri' + ]) + headers = {'X-Goog-FieldMask': field_mask} + + try: + response = self.session.post(PLACES_SEARCH_URL, json=body, headers=headers, timeout=15) + response.raise_for_status() + return response.json().get('places', []) + except requests.exceptions.RequestException as e: + logger.error(f"Places raw search error for '{query}': {e}") + return [] + def search_nearby(self, latitude: float, longitude: float, radius: float = 5000.0, included_types: List[str] = None, diff --git a/templates/admin/gbp_audit_dashboard.html b/templates/admin/gbp_audit_dashboard.html index ee23a9d..1334ebe 100644 --- a/templates/admin/gbp_audit_dashboard.html +++ b/templates/admin/gbp_audit_dashboard.html @@ -450,6 +450,13 @@
+ + + + + + Dopasuj Place ID + + +
+ +
+ + {% endfor %} + + {% if not companies %} +
+ Wszystkie aktywne firmy maja przypisany Google Place ID. +
+ {% endif %} + +{% endblock %} + +{% block extra_js %} +var csrfToken = '{{ csrf_token() }}'; +var matchedCount = 0; +var totalCompanies = {{ companies|length }}; + +function searchPlace(companyId, companyName) { + var resultsDiv = document.getElementById('results-' + companyId); + var btn = event.target; + btn.disabled = true; + btn.textContent = 'Szukam...'; + + resultsDiv.innerHTML = '
Szukam "' + companyName + '" w Google Maps...
'; + resultsDiv.classList.add('visible'); + + var formData = new FormData(); + formData.append('company_id', companyId); + formData.append('csrf_token', csrfToken); + + fetch('{{ url_for("admin.admin_gbp_search_place") }}', { + method: 'POST', + body: formData + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + btn.disabled = false; + btn.textContent = 'Szukaj w Google'; + + if (data.error) { + resultsDiv.innerHTML = '
Blad: ' + data.error + '
'; + return; + } + + if (!data.results || data.results.length === 0) { + resultsDiv.innerHTML = '
Nie znaleziono wynikow dla "' + data.query + '"
'; + return; + } + + var html = ''; + data.results.forEach(function(p) { + var meta = []; + if (p.rating) meta.push('' + p.rating + ' ★'); + if (p.reviews_count) meta.push(p.reviews_count + ' opinii'); + if (p.types) meta.push(p.types); + + html += '
'; + html += '
'; + html += '
' + p.name + '
'; + html += '
' + p.address + '
'; + if (meta.length) html += '
' + meta.join(' · ') + '
'; + html += '
'; + html += ' '; + html += '
'; + }); + + resultsDiv.innerHTML = html; + }) + .catch(function(err) { + btn.disabled = false; + btn.textContent = 'Szukaj w Google'; + resultsDiv.innerHTML = '
Blad polaczenia
'; + }); +} + +function confirmPlace(companyId, placeId, googleName) { + var btn = event.target; + btn.disabled = true; + btn.textContent = 'Zapisuje...'; + + var formData = new FormData(); + formData.append('company_id', companyId); + formData.append('place_id', placeId); + formData.append('google_name', googleName); + formData.append('csrf_token', csrfToken); + + fetch('{{ url_for("admin.admin_gbp_confirm_place") }}', { + method: 'POST', + body: formData + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.error) { + btn.disabled = false; + btn.textContent = 'Potwierdz'; + alert('Blad: ' + data.error); + return; + } + + var card = document.getElementById('card-' + companyId); + card.classList.add('matched'); + var header = card.querySelector('.company-header'); + var nameDiv = header.querySelector('.company-name'); + nameDiv.innerHTML += ' ' + googleName + ''; + + var resultsDiv = document.getElementById('results-' + companyId); + resultsDiv.classList.remove('visible'); + + var buttons = header.querySelectorAll('button'); + buttons.forEach(function(b) { b.style.display = 'none'; }); + + matchedCount++; + document.getElementById('matchedCount').textContent = matchedCount; + document.getElementById('remainingCount').textContent = totalCompanies - matchedCount; + }) + .catch(function(err) { + btn.disabled = false; + btn.textContent = 'Potwierdz'; + alert('Blad polaczenia'); + }); +} + +function skipCompany(companyId) { + var card = document.getElementById('card-' + companyId); + card.style.display = 'none'; + totalCompanies--; + document.getElementById('remainingCount').textContent = totalCompanies - matchedCount; +} +{% endblock %}