schneespur/resources/views/admin/alerts/index.blade.php
Michael ee3dbba6cc Initial release v1.0.0
Schneespur — Open-source winter service documentation software (PWA + Admin).
GPS tracking via OwnTracks, weather data, photo evidence, and legally
compliant service records for winter maintenance operators.

License: AGPL-3.0-or-later
2026-05-17 13:33:51 +00:00

170 lines
14 KiB
PHP

<x-admin-layout>
<x-slot name="header">{{ __('alert.page_title') }} <x-help-icon topic="owntracks" /></x-slot>
{{-- Summary badges --}}
<div class="mt-4 flex flex-wrap gap-2">
<a href="{{ route('admin.alerts.index', ['type' => 'missing_gps']) }}"
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium {{ ($filters['type'] ?? '') === 'missing_gps' ? 'bg-red-600 text-white' : 'bg-red-100 text-red-800' }}">
{{ __('alert.type_missing_gps') }}
<span class="ml-1.5 inline-flex items-center justify-center rounded-full bg-white/20 px-2 py-0.5 text-xs font-bold">{{ $counts['missing_gps'] }}</span>
</a>
<a href="{{ route('admin.alerts.index', ['type' => 'missing_weather']) }}"
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium {{ ($filters['type'] ?? '') === 'missing_weather' ? 'bg-orange-600 text-white' : 'bg-orange-100 text-orange-800' }}">
{{ __('alert.type_missing_weather') }}
<span class="ml-1.5 inline-flex items-center justify-center rounded-full bg-white/20 px-2 py-0.5 text-xs font-bold">{{ $counts['missing_weather'] }}</span>
</a>
<a href="{{ route('admin.alerts.index', ['type' => 'overdue']) }}"
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium {{ ($filters['type'] ?? '') === 'overdue' ? 'bg-yellow-600 text-white' : 'bg-yellow-100 text-yellow-800' }}">
{{ __('alert.type_overdue') }}
<span class="ml-1.5 inline-flex items-center justify-center rounded-full bg-white/20 px-2 py-0.5 text-xs font-bold">{{ $counts['overdue'] }}</span>
</a>
</div>
{{-- Filters --}}
<form method="GET" action="{{ route('admin.alerts.index') }}" class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
<div>
<label for="type" class="block text-sm font-medium text-gray-700">{{ __('alert.filter_type') }}</label>
<select name="type" id="type" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="">{{ __('alert.filter_type_all') }}</option>
<option value="missing_gps" @selected(($filters['type'] ?? '') === 'missing_gps')>{{ __('alert.type_missing_gps') }}</option>
<option value="missing_weather" @selected(($filters['type'] ?? '') === 'missing_weather')>{{ __('alert.type_missing_weather') }}</option>
<option value="overdue" @selected(($filters['type'] ?? '') === 'overdue')>{{ __('alert.type_overdue') }}</option>
</select>
</div>
<div>
<label for="date_from" class="block text-sm font-medium text-gray-700">{{ __('alert.filter_date_from') }}</label>
<input type="date" name="date_from" id="date_from" value="{{ $filters['date_from'] ?? '' }}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="date_to" class="block text-sm font-medium text-gray-700">{{ __('alert.filter_date_to') }}</label>
<input type="date" name="date_to" id="date_to" value="{{ $filters['date_to'] ?? '' }}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700">{{ __('alert.filter_status') }}</label>
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="open" @selected(($filters['status'] ?? 'open') === 'open')>{{ __('alert.filter_status_open') }}</option>
<option value="resolved" @selected(($filters['status'] ?? '') === 'resolved')>{{ __('alert.filter_status_resolved') }}</option>
</select>
</div>
<div class="flex items-end gap-2">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
{{ __('alert.filter_btn') }}
</button>
<a href="{{ route('admin.alerts.index') }}" class="text-sm text-gray-600 hover:text-gray-900 underline">{{ __('alert.filter_reset') }}</a>
</div>
</form>
{{-- Alert list --}}
<div class="mt-6">
@if (!isset($filters['type']) || !$filters['type'])
{{-- No type selected yet --}}
<x-empty-state :heading="__('alert.select_type_heading')" :body="__('alert.select_type_body')" />
@elseif ($alerts && $alerts->count())
@php
$isResolved = ($filters['status'] ?? '') === 'resolved';
@endphp
{{-- Bulk resolve button for open alerts --}}
@if (!$isResolved && $alerts->count() > 1)
<div class="mb-4">
<form method="POST" action="{{ route('admin.alerts.bulk-resolve') }}" x-data
x-on:submit.prevent="if(confirm('{{ __('alert.bulk_confirm') }}')) $el.submit()">
@csrf
<input type="hidden" name="type" value="{{ $filters['type'] }}">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 focus:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150">
{{ __('alert.btn_bulk_resolve') }} ({{ $counts[$filters['type']] ?? 0 }})
</button>
</form>
</div>
@endif
<div class="bg-white overflow-hidden shadow-sm rounded-lg">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_job') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_customer') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_driver') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_type') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_date') }}</th>
@if ($isResolved)
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_resolved_at') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_resolved_by') }}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_note') }}</th>
@else
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('alert.col_actions') }}</th>
@endif
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($alerts as $alert)
@php
$job = $isResolved ? $alert->job : $alert;
@endphp
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<a href="{{ route('admin.jobs.show', $job) }}" class="text-indigo-600 hover:text-indigo-900">
#{{ $job->id }} — {{ $job->localStartedAt()->format('d.m.Y H:i') }}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $job->customer->name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $job->user->name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
@php
$typeBadgeClass = match($filters['type']) {
'missing_gps' => 'bg-red-100 text-red-800',
'missing_weather' => 'bg-orange-100 text-orange-800',
'overdue' => 'bg-yellow-100 text-yellow-800',
default => 'bg-gray-100 text-gray-800',
};
@endphp
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $typeBadgeClass }}">
{{ __('alert.type_' . $filters['type']) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $job->localStartedAt()->format('d.m.Y H:i') }}</td>
@if ($isResolved)
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $alert->resolved_at->setTimezone(config('app.display_timezone'))->format('d.m.Y H:i') }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $alert->resolvedBy?->name ?? '—' }}</td>
<td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">{{ $alert->note ?? __('alert.no_note') }}</td>
@else
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" x-data="{ showForm: false }">
<button x-show="!showForm" x-on:click="showForm = true" type="button" class="text-indigo-600 hover:text-indigo-900">
{{ __('alert.btn_resolve') }}
</button>
<div x-show="showForm" x-cloak class="text-left">
<form method="POST" action="{{ route('admin.alerts.resolve', $job) }}">
@csrf
<input type="hidden" name="alert_type" value="{{ $filters['type'] }}">
<div class="mb-2">
<label class="block text-xs font-medium text-gray-700">{{ __('alert.resolve_note_label') }}</label>
<textarea name="note" rows="2" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="{{ __('alert.resolve_note_placeholder') }}"></textarea>
</div>
<div class="flex gap-2">
<button type="submit" class="inline-flex items-center px-3 py-1.5 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-500 focus:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
{{ __('alert.btn_resolve_submit') }}
</button>
<button type="button" x-on:click="showForm = false" class="text-xs text-gray-600 hover:text-gray-900 underline">
{{ __('alert.btn_resolve_cancel') }}
</button>
</div>
</form>
</div>
</td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="mt-4">
{{ $alerts->links() }}
</div>
@else
<x-empty-state :heading="__('alert.empty_heading')" :body="__('alert.empty_filtered')" />
@endif
</div>
</x-admin-layout>