Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
196
resources/views/admin/locations/index.blade.php
Executable file
196
resources/views/admin/locations/index.blade.php
Executable file
@@ -0,0 +1,196 @@
|
||||
<x-layouts.admin :title="__('admin.locations_title')">
|
||||
@push('styles')
|
||||
<style>
|
||||
.photon-dropdown { position: absolute; z-index: 10; margin-top: 0.25rem; width: 100%; background: white; border: 1px solid #d1d5db; border-radius: 0.375rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,.1); max-height: 15rem; overflow-y: auto; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('admin.locations_title') }}</h1>
|
||||
|
||||
{{-- Neuen Ort anlegen --}}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 max-w-3xl" x-data="locationForm()">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ __('admin.location_add') }}</h2>
|
||||
<form method="POST" action="{{ route('admin.locations.store') }}">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.location_name_label') }} *</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="{{ __('admin.location_name_placeholder') }}">
|
||||
@error('name')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.search_address') }}</label>
|
||||
<input type="text" x-model="query" @input.debounce.300ms="search()" @keydown.escape="results = []"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="{{ __('admin.search_address_hint') }}" autocomplete="off">
|
||||
<div x-show="results.length > 0" x-cloak @click.outside="results = []" class="photon-dropdown">
|
||||
<template x-for="(r, idx) in results" :key="idx">
|
||||
<button type="button" @click="select(r)" class="w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 text-sm">
|
||||
<span class="block font-medium text-gray-900" x-text="r.title"></span>
|
||||
<span class="block text-xs text-gray-500" x-text="r.subtitle"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.address') }}</label>
|
||||
<input type="text" name="address_text" x-model="addressText"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="{{ __('admin.address_manual_hint') }}">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Lat</label>
|
||||
<input type="text" name="location_lat" x-model="lat" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Lng</label>
|
||||
<input type="text" name="location_lng" x-model="lng" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">{{ __('admin.location_save') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Bestehende Orte --}}
|
||||
@if ($locations->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden overflow-x-auto max-w-3xl">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('admin.location_name_label') }}</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('admin.address') }}</th>
|
||||
<th class="text-right px-4 py-3 font-medium text-gray-700">{{ __('admin.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach ($locations as $location)
|
||||
<tr class="hover:bg-gray-50" x-data="{ editing: false }">
|
||||
{{-- Anzeige-Modus --}}
|
||||
<template x-if="!editing">
|
||||
<td class="px-4 py-3 font-medium text-gray-900">{{ $location->name }}</td>
|
||||
</template>
|
||||
<template x-if="!editing">
|
||||
<td class="px-4 py-3 text-gray-600">
|
||||
{{ $location->address_text ?: '–' }}
|
||||
@if ($location->location_lat)
|
||||
<span class="text-xs text-gray-400 ml-1">({{ number_format($location->location_lat, 4) }}, {{ number_format($location->location_lng, 4) }})</span>
|
||||
@endif
|
||||
</td>
|
||||
</template>
|
||||
<template x-if="!editing">
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<button type="button" @click="editing = true" class="text-xs text-blue-600 hover:text-blue-800 mr-2">{{ __('ui.edit') }}</button>
|
||||
<form method="POST" action="{{ route('admin.locations.destroy', $location) }}" class="inline" onsubmit="return confirm(@js(__('admin.location_confirm_delete')))">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-xs text-red-600 hover:text-red-800">{{ __('ui.delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
{{-- Bearbeitungs-Modus --}}
|
||||
<template x-if="editing">
|
||||
<td colspan="3" class="px-4 py-3" x-data="locationForm(@js($location->address_text ?? ''), @js($location->location_lat), @js($location->location_lng))">
|
||||
<form method="POST" action="{{ route('admin.locations.update', $location) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 items-end">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.location_name_label') }}</label>
|
||||
<input type="text" name="name" value="{{ $location->name }}" required class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.search_address') }}</label>
|
||||
<input type="text" x-model="query" @input.debounce.300ms="search()" @keydown.escape="results = []"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm" autocomplete="off">
|
||||
<div x-show="results.length > 0" x-cloak @click.outside="results = []" class="photon-dropdown">
|
||||
<template x-for="(r, idx) in results" :key="idx">
|
||||
<button type="button" @click="select(r)" class="w-full text-left px-2 py-1.5 hover:bg-blue-50 border-b border-gray-100 text-xs">
|
||||
<span class="block font-medium" x-text="r.title"></span>
|
||||
<span class="block text-gray-500" x-text="r.subtitle"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.address') }}</label>
|
||||
<input type="text" name="address_text" x-model="addressText" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
<input type="hidden" name="location_lat" x-model="lat">
|
||||
<input type="hidden" name="location_lng" x-model="lng">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="bg-blue-600 text-white px-3 py-1.5 rounded-md text-xs font-medium hover:bg-blue-700">{{ __('ui.save') }}</button>
|
||||
<button type="button" @click="editing = false" class="text-xs text-gray-600 hover:underline">{{ __('ui.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">{{ __('admin.locations_empty') }}</p>
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function locationForm(initAddress = '', initLat = '', initLng = '') {
|
||||
return {
|
||||
query: '',
|
||||
results: [],
|
||||
addressText: initAddress || '',
|
||||
lat: initLat || '',
|
||||
lng: initLng || '',
|
||||
_abortCtrl: null,
|
||||
formatFeature(f) {
|
||||
const p = f.properties;
|
||||
const street = [p.street, p.housenumber].filter(Boolean).join(' ');
|
||||
const effectiveStreet = street || (p.name && p.name !== p.city ? p.name : '');
|
||||
const cityLine = [p.postcode, p.city].filter(Boolean).join(' ');
|
||||
const address = [effectiveStreet, cityLine].filter(Boolean).join(', ');
|
||||
const name = p.name || '';
|
||||
const isPlace = name && name !== effectiveStreet && name !== p.city && name !== p.street;
|
||||
return {
|
||||
title: isPlace ? name : (address || name || ''),
|
||||
subtitle: isPlace ? address : (cityLine || p.state || ''),
|
||||
address: address || name || '',
|
||||
lat: f.geometry.coordinates[1],
|
||||
lon: f.geometry.coordinates[0],
|
||||
};
|
||||
},
|
||||
async search() {
|
||||
const q = this.query.trim();
|
||||
if (q.length < 2) { this.results = []; return; }
|
||||
if (this._abortCtrl) this._abortCtrl.abort();
|
||||
this._abortCtrl = new AbortController();
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: q, lang: 'de', limit: '7',
|
||||
lat: '51.4', lon: '7.5',
|
||||
bbox: '5.87,50.32,9.46,52.53',
|
||||
});
|
||||
const resp = await fetch('https://photon.komoot.io/api/?' + params, { signal: this._abortCtrl.signal });
|
||||
const data = await resp.json();
|
||||
this.results = (data.features || []).map(f => this.formatFeature(f));
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') this.results = [];
|
||||
}
|
||||
},
|
||||
select(r) {
|
||||
this.addressText = r.address;
|
||||
this.lat = r.lat;
|
||||
this.lng = r.lon;
|
||||
this.query = r.address;
|
||||
this.results = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-layouts.admin>
|
||||
Reference in New Issue
Block a user