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
224 lines
10 KiB
PHP
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();">×</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();">×</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
function escAttr(str) {
|
|
if (!str) { return ''; }
|
|
return String(str).replace(/"/g, '"');
|
|
}
|
|
|
|
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
|