OrderRequestResource
Die OrderRequestResource verwaltet das Matching zwischen Aufträgen und Sprachmittlern.
Konfiguration
File: OrderRequestResource.php
| Eigenschaft | Wert |
|---|---|
| Model | App\Models\OrderRequest |
| Navigation Group | Auftragsverwaltung |
| Navigation Label | Sprachmittler-Matching |
| Navigation Icon | heroicon-o-chat-bubble-left-right |
| Navigation Sort | 3 |
| Polling Interval | 30 Sekunden |
Navigation Badge
public static function getNavigationBadge(): ?string
{
return static::getModel()::query()
->where('state', Requested::class)
->where('timeout_at', '>', now())
->count();
}
public static function getNavigationBadgeColor(): ?string
{
return 'warning';
}
State Machine
State-Farben
| State | Farbe |
|---|---|
| Requested | warning |
| Accepted | success |
| Rejected | danger |
| Expired | gray |
| AutomaticallyCancelled | gray |
Form-Struktur
Basis-Daten
| Feld | Typ | Beschreibung |
|---|---|---|
| order_id | Select | Zugehöriger Auftrag |
| interpreter_id | Select | Sprachmittler (mit Distanz) |
| state | Select | Status |
| request_type | Select | adhoc/planned |
Forms\Components\Select::make('interpreter_id')
->relationship('interpreter', 'full_name')
->getOptionLabelFromRecordUsing(function (User $record, Get $get) {
$order = Order::find($get('order_id'));
if ($order && $record->hasCoordinates()) {
$distance = $record->distanceFrom(
$order->location_latitude,
$order->location_longitude
);
return "{$record->full_name} ({$distance} km)";
}
return $record->full_name;
})
Sprache & Service
| Feld | Typ |
|---|---|
| source_language | Select |
| target_language | Select |
| service_type | Select (interpretation/translation/both) |
Matching & Timeouts
| Feld | Typ | Beschreibung |
|---|---|---|
| score | TextInput | Matching-Score (0-100) |
| is_first_choice | Toggle | Erste Wahl |
| is_urgent | Toggle | Dringend |
| timeout_at | DateTimePicker | Timeout-Zeitpunkt |
| timeout_minutes | TextInput | Timeout in Minuten |
| attempt_number | TextInput | Versuchsnummer |
Response & Admin Notes
| Feld | Typ |
|---|---|
| responded_at | DateTimePicker |
| rejection_reason | Textarea |
| admin_notes | Textarea |
Infolist-Konfiguration
Für die View-Seite wird ein Infolist verwendet:
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
Infolists\Components\Section::make('Links')
->columns(3)
->schema([
Infolists\Components\TextEntry::make('order.order_number')
->url(fn ($record) => OrderResource::getUrl('view', ['record' => $record->order_id])),
Infolists\Components\TextEntry::make('order.requester.full_name')
->url(fn ($record) => UserResource::getUrl('view', ['record' => $record->order->requester_id])),
Infolists\Components\TextEntry::make('interpreter.full_name')
->url(fn ($record) => UserResource::getUrl('view', ['record' => $record->interpreter_id])),
]),
Infolists\Components\Section::make('Status')
->schema([
Infolists\Components\TextEntry::make('state')
->badge()
->color(fn (string $state) => match ($state) {
'requested' => 'warning',
'accepted' => 'success',
'rejected' => 'danger',
default => 'gray',
}),
Infolists\Components\TextEntry::make('score')
->badge()
->color(fn (int $score) => match (true) {
$score >= 80 => 'success',
$score >= 60 => 'warning',
default => 'danger',
}),
Infolists\Components\TextEntry::make('distance')
->suffix(' km'),
]),
]);
}
Table-Konfiguration
Spalten
| Spalte | Typ | Formatierung |
|---|---|---|
| order.order_number | TextColumn | Link zur Order |
| order.requester.full_name | TextColumn | Link zum User |
| interpreter.full_name | TextColumn | Link zum User |
| state | BadgeColumn | Farbcodiert |
| request_type | BadgeColumn | adhoc=info, planned=success |
| source_language | BadgeColumn | - |
| target_language | BadgeColumn | - |
| score | TextColumn | Farbcodiert (≥80=success, ≥60=warning, sonst danger) |
| distance | TextColumn | Mit "km" Suffix |
| timeout_at | TextColumn | Mit Expired-Indikator |
| start_time | TextColumn | Formatiert |
| created_at | TextColumn | Sortiert desc |
Score-Farbcodierung
Tables\Columns\TextColumn::make('score')
->badge()
->color(fn (int $state): string => match (true) {
$state >= 80 => 'success',
$state >= 60 => 'warning',
default => 'danger',
})
Filter
| Filter | Typ | Beschreibung |
|---|---|---|
| state | SelectFilter | Status-Auswahl |
| request_type | SelectFilter | Anfragetyp |
| is_urgent | TernaryFilter | Nur dringende |
| is_active | TernaryFilter | Requested + nicht abgelaufen |
Tables\Filters\Filter::make('is_active')
->label('Nur aktive')
->query(fn (Builder $query) => $query
->where('state', Requested::class)
->where('timeout_at', '>', now())
)
Actions
resend_notification
Sendet die Benachrichtigung erneut an den Sprachmittler:
Action::make('resend_notification')
->label('Benachrichtigung erneut senden')
->icon('heroicon-o-bell-alert')
->requiresConfirmation()
->action(function (OrderRequest $record) {
if ($record->is_urgent) {
$record->interpreter->notify(new UrgentOrderAvailable($record->order));
} else {
$record->interpreter->notify(new RegularOrderAvailable($record->order));
}
Notification::make()
->success()
->title('Benachrichtigung gesendet')
->body("An {$record->interpreter->full_name}")
->send();
})
accept
Akzeptiert die Anfrage im Namen des Sprachmittlers:
Action::make('accept')
->label('Akzeptieren')
->icon('heroicon-o-check')
->color('success')
->requiresConfirmation()
->modalHeading('Anfrage akzeptieren')
->modalDescription('Der Auftrag wird dem Sprachmittler zugewiesen.')
->action(function (OrderRequest $record) {
ProcessOrderRequestResponse::run($record, 'accepted');
Notification::make()
->success()
->title('Anfrage akzeptiert')
->send();
})
reject
Lehnt die Anfrage ab (mit Begründung):
Action::make('reject')
->label('Ablehnen')
->icon('heroicon-o-x-mark')
->color('danger')
->form([
Forms\Components\Textarea::make('rejection_reason')
->label('Ablehnungsgrund')
->required(),
])
->action(function (OrderRequest $record, array $data) {
$record->update(['rejection_reason' => $data['rejection_reason']]);
ProcessOrderRequestResponse::run($record, 'rejected');
Notification::make()
->success()
->title('Anfrage abgelehnt')
->send();
})
Auto-Refresh
Die Tabelle aktualisiert sich automatisch alle 30 Sekunden:
protected static ?string $pollingInterval = '30s';
Matching-Algorithmus
Der Matching-Score wird basierend auf folgenden Faktoren berechnet:
| Faktor | Gewichtung |
|---|---|
| Distanz | 40% |
| Sprachkenntnisse | 30% |
| Verfügbarkeit | 20% |
| Bisherige Bewertungen | 10% |
// In MatchInterpretersForOrder Action
$score = 0;
// Distanz (max 40 Punkte, 0km = 40, 100km = 0)
$distanceScore = max(0, 40 - ($distance / 2.5));
$score += $distanceScore;
// Sprachkenntnisse (max 30 Punkte)
if ($interpreter->hasLanguage($order->target_language)) {
$score += 30;
}
// Verfügbarkeit (max 20 Punkte)
if ($interpreter->isAvailableAt($order->scheduled_at)) {
$score += 20;
}
// Bewertungen (max 10 Punkte)
$avgRating = $interpreter->average_rating; // 1-5
$score += ($avgRating / 5) * 10;
Timeout-System
Anfragen haben ein konfigurierbares Timeout:
// In SystemSettings
'order_request_timeout_minutes' => 30
// Bei Erstellung
$orderRequest->timeout_at = now()->addMinutes(
app(SystemSettings::class)->order_request_timeout_minutes
);
// Scheduled Command prüft abgelaufene Anfragen
// ExpireOrderRequests::class runs every minute
Pages
| Page | Beschreibung |
|---|---|
| index | Übersichtstabelle mit Auto-Refresh |
| view | Infolist-Ansicht |
| edit | Bearbeitung (selten benötigt) |