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
This commit is contained in:
parent
389d5d1820
commit
e3dc1afd8e
165 changed files with 21914 additions and 3516 deletions
409
resources/views/contact/index.blade.php
Normal file
409
resources/views/contact/index.blade.php
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
@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">
|
||||
{{ __('Kontakte') }}
|
||||
<small class="text-muted font-weight-light ml-2 d-none d-md-inline" id="header-subtitle">Stammkunden ohne Duplikate</small>
|
||||
</h4>
|
||||
<a href="{{ route('contact_detail', ['new']) }}" class="btn btn-sm btn-primary" id="btn-new-contact">
|
||||
<span class="fa fa-plus mr-1"></span> {{ __('Neuer Kontakt') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filterleiste --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="form-row align-items-end">
|
||||
|
||||
<div class="form-group col-sm-4 col-md-3 mb-2">
|
||||
<label class="form-label small text-muted mb-1">{{ __('Schnellsuche') }}</label>
|
||||
<input type="text" id="filter-search" class="form-control form-control-sm"
|
||||
placeholder="Name, Vorname, E-Mail, Telefon…">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-sm-3 col-md-2 mb-2">
|
||||
<label class="form-label small text-muted mb-1">{{ __('PLZ / Ort') }}</label>
|
||||
<input type="text" id="filter-location" class="form-control form-control-sm"
|
||||
placeholder="z.B. 80331 oder München">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-sm-5 col-md-4 mb-2">
|
||||
<label class="form-label small text-muted mb-1">{{ __('Anzeigen') }}</label>
|
||||
<div class="btn-group btn-group-sm d-flex" id="filter-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary filter-btn active" data-filter="">
|
||||
Alle
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="has_leads">
|
||||
Mit Anfragen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="has_bookings">
|
||||
Mit Buchungen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning filter-btn" data-filter="deleted">
|
||||
<span class="fa fa-trash mr-1"></span> Papierkorb
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-auto mb-2 ml-auto">
|
||||
<button type="button" id="filter-reset" class="btn btn-sm btn-outline-danger">
|
||||
<span class="fa fa-times mr-1"></span> Zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- History-Modal (Anfragen + Buchungen) --}}
|
||||
<div class="modal fade" id="contactHistoryModal" tabindex="-1" role="dialog" aria-labelledby="contactHistoryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="contactHistoryModalLabel">
|
||||
<i class="fa fa-history mr-2"></i>
|
||||
<span id="history-modal-title">Anfragen & Buchungen</span>
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Schließen">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="history-modal-body">
|
||||
<div class="text-center py-4">
|
||||
<span class="fa fa-spinner fa-spin fa-2x text-muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Bestätigungs-Modal Löschen --}}
|
||||
<div class="modal fade" id="deleteContactModal" tabindex="-1" role="dialog" aria-labelledby="deleteContactModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteContactModalLabel">
|
||||
<i class="fa fa-exclamation-triangle text-danger mr-2"></i>Kontakt löschen
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Schließen">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Soll <strong id="delete-contact-name"></strong> wirklich gelöscht werden?</p>
|
||||
<p class="text-muted small mb-0">Der Kontakt wird als gelöscht markiert und kann bei Bedarf
|
||||
wiederhergestellt werden.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-danger" id="btn-confirm-delete">
|
||||
<span class="fa fa-trash mr-1"></span> Endgültig löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fehler-Toast --}}
|
||||
<div id="delete-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="$('#delete-error-toast').fadeOut();">×</button>
|
||||
<span id="delete-error-message"></span>
|
||||
</div>
|
||||
|
||||
{{-- Erfolg-Toast (Wiederherstellen) --}}
|
||||
<div id="restore-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="$('#restore-success-toast').fadeOut();">×</button>
|
||||
<span class="fa fa-check mr-1"></span> Kontakt wurde wiederhergestellt.
|
||||
</div>
|
||||
|
||||
{{-- Tabelle --}}
|
||||
<div class="card">
|
||||
<div class="table-responsive-track" id="datatables-contact-scroll">
|
||||
<div class="table-responsive-thumb" id="datatables-contact-thumb"></div>
|
||||
</div>
|
||||
<div class="card-datatable table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm" id="datatables-contact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 42px;"> </th>
|
||||
<th style="width: 70px;">{{ __('ID') }}</th>
|
||||
<th>{{ __('Vorname') }}</th>
|
||||
<th>{{ __('Nachname') }}</th>
|
||||
<th>{{ __('E-Mail') }}</th>
|
||||
<th>{{ __('PLZ') }}</th>
|
||||
<th>{{ __('Ort') }}</th>
|
||||
<th style="width: 80px;" class="text-center">{{ __('Anfragen') }}</th>
|
||||
<th style="width: 80px;" class="text-center">{{ __('Buchungen') }}</th>
|
||||
<th style="width: 130px;">{{ __('Gelöscht am') }}</th>
|
||||
<th style="width: 42px;"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
var activeFilter = '';
|
||||
var locationFilter = '';
|
||||
|
||||
var table = $('#datatables-contact').dataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": {
|
||||
"url": '{!! route('data_table_contacts') !!}',
|
||||
"data": function(d) {
|
||||
if (activeFilter === 'has_leads') {
|
||||
d.filter_has_leads = 1;
|
||||
}
|
||||
if (activeFilter === 'has_bookings') {
|
||||
d.filter_has_bookings = 1;
|
||||
}
|
||||
if (activeFilter === 'deleted') {
|
||||
d.filter_deleted = 1;
|
||||
}
|
||||
if (locationFilter !== '') {
|
||||
d.filter_location = locationFilter;
|
||||
}
|
||||
}
|
||||
},
|
||||
"order": [
|
||||
[1, "desc"]
|
||||
],
|
||||
"columns": [{
|
||||
data: 'action_edit',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
name: 'customer.id'
|
||||
},
|
||||
{
|
||||
data: 'firstname',
|
||||
name: 'firstname',
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
data: 'name',
|
||||
name: 'name',
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
data: 'email',
|
||||
name: 'email',
|
||||
searchable: true
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
data: 'zip',
|
||||
name: 'zip',
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
data: 'city',
|
||||
name: 'city',
|
||||
searchable: true
|
||||
|
||||
},
|
||||
{
|
||||
data: 'leads_count',
|
||||
name: 'leads_count',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: 'text-center'
|
||||
},
|
||||
{
|
||||
data: 'bookings_count',
|
||||
name: 'bookings_count',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: 'text-center'
|
||||
},
|
||||
{
|
||||
data: 'deleted_at',
|
||||
name: 'deleted_at',
|
||||
searchable: false,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'action_delete',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
},
|
||||
],
|
||||
"columnDefs": [{
|
||||
// Anzahl-Badges — klickbar für History-Modal
|
||||
targets: [7, 8],
|
||||
render: function(data, type, row) {
|
||||
if (data === null || parseInt(data) === 0) {
|
||||
return '<span class="text-muted">' + (data !== null ? data : '—') + '</span>';
|
||||
}
|
||||
return '<span class="badge badge-pill badge-primary badge-history-link" '
|
||||
+ 'style="cursor:pointer;" data-id="' + row.raw_id + '">'
|
||||
+ data + '</span>';
|
||||
}
|
||||
}],
|
||||
"bLengthChange": false,
|
||||
"iDisplayLength": 100,
|
||||
"language": {
|
||||
"url": "/js/German.json"
|
||||
},
|
||||
"dom": 'rt<"d-flex justify-content-between mt-2"ip>',
|
||||
drawCallback: function() {
|
||||
dataTableScrollTrack('#datatables-contact');
|
||||
}
|
||||
}).api();
|
||||
|
||||
// ── Schnellsuche (Globale Suche) ──────────────────────────────────
|
||||
var searchTimer;
|
||||
$('#filter-search').on('keyup', function() {
|
||||
clearTimeout(searchTimer);
|
||||
var val = $(this).val();
|
||||
searchTimer = setTimeout(function() {
|
||||
table.search(val).draw();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
// ── PLZ / Ort Suche (OR über zip + city, serverseitig) ───────────
|
||||
var locationTimer;
|
||||
$('#filter-location').on('keyup', function() {
|
||||
clearTimeout(locationTimer);
|
||||
locationFilter = $(this).val();
|
||||
locationTimer = setTimeout(function() {
|
||||
table.draw();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
// ── Schnellfilter-Buttons ─────────────────────────────────────────
|
||||
$('.filter-btn').on('click', function() {
|
||||
$('.filter-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
activeFilter = $(this).data('filter');
|
||||
var isTrash = (activeFilter === 'deleted');
|
||||
$('#btn-new-contact').toggle(!isTrash);
|
||||
$('#header-subtitle').text(isTrash ? 'Gelöschte Kontakte' : 'Stammkunden ohne Duplikate');
|
||||
table.column(9).visible(isTrash);
|
||||
if (isTrash) {
|
||||
table.order([9, 'desc']).draw();
|
||||
} else {
|
||||
table.order([1, 'desc']).draw();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Zurücksetzen ──────────────────────────────────────────────────
|
||||
$('#filter-reset').on('click', function() {
|
||||
activeFilter = '';
|
||||
locationFilter = '';
|
||||
$('#filter-search').val('');
|
||||
$('#filter-location').val('');
|
||||
$('.filter-btn').removeClass('active');
|
||||
$('.filter-btn[data-filter=""]').addClass('active');
|
||||
$('#btn-new-contact').show();
|
||||
$('#header-subtitle').text('Stammkunden ohne Duplikate');
|
||||
table.column(9).visible(false);
|
||||
table.order([1, 'desc']).search('').columns().search('').draw();
|
||||
});
|
||||
|
||||
// ── Löschen ───────────────────────────────────────────────────────
|
||||
var deleteContactId = null;
|
||||
|
||||
// Klick auf Löschen-Button öffnet Modal
|
||||
$('#datatables-contact').on('click', '.btn-contact-delete', function() {
|
||||
deleteContactId = $(this).data('id');
|
||||
$('#delete-contact-name').text($(this).data('name'));
|
||||
$('#deleteContactModal').modal('show');
|
||||
});
|
||||
|
||||
// Bestätigung: DELETE-Request abschicken
|
||||
$('#btn-confirm-delete').on('click', function() {
|
||||
if (!deleteContactId) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $btn = $(this).prop('disabled', true).html(
|
||||
'<span class="fa fa-spinner fa-spin mr-1"></span> Löschen…');
|
||||
|
||||
$.ajax({
|
||||
url: '/contact/' + deleteContactId,
|
||||
type: 'POST',
|
||||
data: {
|
||||
_method: 'DELETE',
|
||||
_token: '{{ csrf_token() }}'
|
||||
},
|
||||
success: function() {
|
||||
$('#deleteContactModal').modal('hide');
|
||||
table.draw(false); // Seite behalten, nur neu laden
|
||||
},
|
||||
error: function(xhr) {
|
||||
$('#deleteContactModal').modal('hide');
|
||||
var msg = (xhr.responseJSON && xhr.responseJSON.message) ?
|
||||
xhr.responseJSON.message :
|
||||
'Löschen fehlgeschlagen.';
|
||||
$('#delete-error-message').text(msg);
|
||||
$('#delete-error-toast').show().addClass('show');
|
||||
setTimeout(function() {
|
||||
$('#delete-error-toast').removeClass('show').fadeOut();
|
||||
}, 6000);
|
||||
},
|
||||
complete: function() {
|
||||
$btn.prop('disabled', false).html(
|
||||
'<span class="fa fa-trash mr-1"></span> Endgültig löschen');
|
||||
deleteContactId = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Wiederherstellen ──────────────────────────────────────────────
|
||||
$('#datatables-contact').on('click', '.btn-contact-restore', function() {
|
||||
var $btn = $(this).prop('disabled', true).html('<span class="fa fa-spinner fa-spin"></span>');
|
||||
var contactId = $(this).data('id');
|
||||
|
||||
$.ajax({
|
||||
url: '/contact/' + contactId + '/restore',
|
||||
type: 'POST',
|
||||
data: {
|
||||
_method: 'PATCH',
|
||||
_token: '{{ csrf_token() }}'
|
||||
},
|
||||
success: function() {
|
||||
table.draw(false);
|
||||
$('#restore-success-toast').show().addClass('show');
|
||||
setTimeout(function() {
|
||||
$('#restore-success-toast').removeClass('show').fadeOut();
|
||||
}, 4000);
|
||||
},
|
||||
error: function() {
|
||||
$btn.prop('disabled', false).html('<span class="fa fa-undo"></span>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── History-Modal (Anfragen & Buchungen) ──────────────────────────
|
||||
$('#datatables-contact').on('click', '.badge-history-link', function() {
|
||||
var contactId = $(this).data('id');
|
||||
$('#history-modal-body').html(
|
||||
'<div class="text-center py-4"><span class="fa fa-spinner fa-spin fa-2x text-muted"></span></div>'
|
||||
);
|
||||
$('#contactHistoryModal').modal('show');
|
||||
$.get('/contact/' + contactId + '/history', function(html) {
|
||||
$('#history-modal-body').html(html);
|
||||
}).fail(function() {
|
||||
$('#history-modal-body').html(
|
||||
'<p class="text-danger p-3">Fehler beim Laden der Daten.</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Loading…
Add table
Add a link
Reference in a new issue