mein-sterntours/resources/views/contact/duplicates.blade.php
Phase-1-Rollback-Agent e3dc1afd8e WIP: Sicherheitsnetz vor Phase-1-R\u00fcckbau
Enth\u00e4lt gemischt: Laravel-10-Upgrade + Phase 1 (Contacts-Modul, Duplicats-Commands,
Soft-Delete+Merge-Fields) + Phase 2 Code-Umstellungen (inquiry_id, $table='contacts'/'inquiries')
+ Offers-Modul (Migrationen, Models, offer_id in Booking, offer-Disk in filesystems.php).

Phase 2 + Offers werden im folgenden Commit nach dev/backups/phase2-offers-2026-04-17/
verschoben, damit der Workspace auf Phase-1-only (= Test-System-Stand) reduziert ist
und direkt auf Live deploybar wird.

Tarball-Backup zus\u00e4tzlich unter: ../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz

Made-with: Cursor
2026-04-17 13:40:31 +00:00

224 lines
10 KiB
PHP

@extends('layouts.layout-2')
@section('content')
<div class="d-flex justify-content-between align-items-center py-3 mb-3">
<h4 class="font-weight-bold mb-0">
Mögliche Duplikate
<small class="text-muted font-weight-light ml-2 d-none d-md-inline">Kontakte zur manuellen Prüfung</small>
</h4>
<a href="{{ route('contacts') }}" class="btn btn-sm btn-outline-secondary">
<span class="fa fa-arrow-left mr-1"></span> Zurück zur Übersicht
</a>
</div>
{{-- Konfidenz-Tabs --}}
<ul class="nav nav-tabs mb-3" id="confidence-tabs">
<li class="nav-item">
<a class="nav-link active" href="#" data-confidence="HIGH">
<span class="badge badge-success mr-1">{{ $counts['HIGH'] }}</span>
HIGH gleiche E-Mail
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-confidence="MEDIUM">
<span class="badge badge-warning mr-1">{{ $counts['MEDIUM'] }}</span>
MEDIUM Name + Geburtsdatum
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-confidence="LOW">
<span class="badge badge-danger mr-1">{{ $counts['LOW'] }}</span>
LOW Name + PLZ
</a>
</li>
</ul>
{{-- Lade-Spinner --}}
<div id="loading" class="text-center py-5">
<span class="fa fa-spinner fa-spin fa-2x text-muted"></span>
<p class="text-muted mt-2">Duplikate werden geladen…</p>
</div>
{{-- Gruppen-Container --}}
<div id="groups-container" style="display:none;"></div>
{{-- Leer-Zustand --}}
<div id="no-duplicates" class="text-center py-5" style="display:none;">
<span class="fa fa-check-circle fa-3x text-success mb-3 d-block"></span>
<p class="text-muted">Keine Duplikate in dieser Kategorie.</p>
</div>
{{-- Erfolg-Toast --}}
<div id="merge-success-toast" class="alert alert-success alert-dismissible fade"
style="position:fixed; bottom:1rem; right:1rem; z-index:9999; min-width:300px; display:none;">
<button type="button" class="close" onclick="$('#merge-success-toast').fadeOut();">&times;</button>
<span class="fa fa-check mr-1"></span> Kontakte erfolgreich zusammengeführt.
</div>
{{-- Fehler-Toast --}}
<div id="merge-error-toast" class="alert alert-danger alert-dismissible fade"
style="position:fixed; bottom:1rem; right:1rem; z-index:9999; min-width:300px; display:none;">
<button type="button" class="close" onclick="$('#merge-error-toast').fadeOut();">&times;</button>
<span id="merge-error-message"></span>
</div>
<script>
var currentConfidence = 'HIGH';
var csrfToken = '{{ csrf_token() }}';
function loadGroups(confidence) {
currentConfidence = confidence;
$('#groups-container').hide().empty();
$('#no-duplicates').hide();
$('#loading').show();
$.get('{{ route('data_contacts_duplicates') }}', { confidence: confidence }, function(groups) {
$('#loading').hide();
if (!groups || groups.length === 0) {
$('#no-duplicates').show();
return;
}
groups.forEach(function(group) {
$('#groups-container').append(renderGroup(group));
});
$('#groups-container').show();
}).fail(function() {
$('#loading').hide();
showError('Fehler beim Laden der Duplikate.');
});
}
function renderGroup(group) {
var master = group.master;
var dupes = group.duplicates;
var html = '<div class="card mb-3 border-left-primary" style="border-left: 4px solid #5a8dee;">';
html += '<div class="card-header py-2 d-flex justify-content-between align-items-center">';
html += '<span class="font-weight-bold"><span class="fa fa-copy mr-1 text-muted"></span>';
html += escHtml(master.firstname + ' ' + master.name);
html += '</span>';
html += '<span class="text-muted small">' + dupes.length + ' Duplikat(e)</span>';
html += '</div>';
html += '<div class="card-body py-2">';
html += '<div class="row">';
// Master-Karte
html += renderContactCard(master, true);
// Duplikat-Karten
dupes.forEach(function(dupe) {
html += renderContactCard(dupe, false, master.id);
});
html += '</div></div></div>';
return html;
}
function renderContactCard(contact, isMaster, masterId) {
var borderClass = isMaster ? 'border-success' : 'border-warning';
var badgeHtml = isMaster
? '<span class="badge badge-success">Master</span>'
: '<span class="badge badge-warning">Duplikat</span>';
var html = '<div class="col-md-6 col-lg-4 mb-2">';
html += '<div class="card h-100 ' + borderClass + '">';
html += '<div class="card-header py-1 d-flex justify-content-between align-items-center">';
html += '<span class="small font-weight-bold">#' + contact.id + ' ' + badgeHtml + '</span>';
html += '<a href="/contact/detail/' + contact.id + '" target="_blank" class="btn btn-xs btn-outline-primary" title="Öffnen"><span class="fa fa-external-link-alt"></span></a>';
html += '</div>';
html += '<div class="card-body py-2 small">';
html += '<table class="table table-sm table-borderless mb-0">';
html += '<tr><td class="text-muted pr-2" style="width:90px;">Name</td><td>' + escHtml(contact.firstname + ' ' + contact.name) + '</td></tr>';
html += '<tr><td class="text-muted">E-Mail</td><td>' + escHtml(contact.email || '—') + '</td></tr>';
html += '<tr><td class="text-muted">PLZ / Ort</td><td>' + escHtml((contact.zip || '') + ' ' + (contact.city || '')) + '</td></tr>';
html += '<tr><td class="text-muted">Telefon</td><td>' + escHtml(contact.phone || contact.phonemobile || '—') + '</td></tr>';
html += '<tr><td class="text-muted">Anfragen</td><td>' + (contact.leads_count || 0) + '</td></tr>';
html += '<tr><td class="text-muted">Buchungen</td><td>' + (contact.bookings_count || 0) + '</td></tr>';
html += '<tr><td class="text-muted">Erstellt</td><td>' + formatDate(contact.created_at) + '</td></tr>';
html += '</table>';
html += '</div>';
if (!isMaster) {
html += '<div class="card-footer py-1 text-right">';
html += '<button class="btn btn-sm btn-warning btn-merge" ';
html += 'data-master="' + masterId + '" data-dupe="' + contact.id + '" ';
html += 'data-name="' + escAttr(contact.firstname + ' ' + contact.name) + '">';
html += '<span class="fa fa-compress-arrows-alt mr-1"></span> In Master zusammenführen';
html += '</button>';
html += '</div>';
}
html += '</div></div>';
return html;
}
function formatDate(dateStr) {
if (!dateStr) { return '—'; }
var d = new Date(dateStr);
return d.toLocaleDateString('de-DE');
}
function escHtml(str) {
if (!str) { return ''; }
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function escAttr(str) {
if (!str) { return ''; }
return String(str).replace(/"/g, '&quot;');
}
function showError(msg) {
$('#merge-error-message').text(msg);
$('#merge-error-toast').show().addClass('show');
setTimeout(function() { $('#merge-error-toast').removeClass('show').fadeOut(); }, 6000);
}
// ── Tab-Wechsel ───────────────────────────────────────────────────────
$('#confidence-tabs a').on('click', function(e) {
e.preventDefault();
$('#confidence-tabs a').removeClass('active');
$(this).addClass('active');
loadGroups($(this).data('confidence'));
});
// ── Zusammenführen ────────────────────────────────────────────────────
$(document).on('click', '.btn-merge', function() {
var $btn = $(this);
var masterId = $btn.data('master');
var dupeId = $btn.data('dupe');
var name = $btn.data('name');
if (!confirm('"' + name + '" als Duplikat in Master #' + masterId + ' zusammenführen?\n\nAlle Anfragen und Buchungen werden umgehängt.')) {
return;
}
$btn.prop('disabled', true).html('<span class="fa fa-spinner fa-spin mr-1"></span> Wird zusammengeführt…');
$.ajax({
url: '{{ route('contact_merge') }}',
type: 'POST',
data: { master_id: masterId, duplicate_id: dupeId, _token: csrfToken },
success: function() {
$('#merge-success-toast').show().addClass('show');
setTimeout(function() { $('#merge-success-toast').removeClass('show').fadeOut(); }, 4000);
// Gruppe aus DOM entfernen und neu laden
loadGroups(currentConfidence);
},
error: function(xhr) {
var msg = (xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Zusammenführen fehlgeschlagen.';
showError(msg);
$btn.prop('disabled', false).html('<span class="fa fa-compress-arrows-alt mr-1"></span> In Master zusammenführen');
}
});
});
// ── Initial laden ─────────────────────────────────────────────────────
$(document).ready(function() {
loadGroups('HIGH');
});
</script>
@endsection