Files
WebAPP/resources/views/admin/locations/index.blade.php
Rhino 2e24a40d68 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>
2026-03-02 07:30:37 +01:00

197 lines
13 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>