feat: complete approval workflow
This commit is contained in:
@@ -9,8 +9,10 @@ use App\Domain\Inventory\Models\SnipeAsset;
|
||||
use App\Domain\Reservations\Enums\ReservationItemStatus;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use App\Domain\Reservations\Models\ReservationItem;
|
||||
use App\Domain\Reservations\Services\AvailabilityService;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ReservationDecisionNotification;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -23,7 +25,7 @@ class ApprovalService
|
||||
{
|
||||
$this->authorize($actor);
|
||||
|
||||
return DB::transaction(function () use ($reservation, $actor, $comment): Reservation {
|
||||
$approved = DB::transaction(function () use ($reservation, $actor, $comment): Reservation {
|
||||
$locked = $this->lockPending($reservation);
|
||||
$items = $locked->items()->with('asset')->get();
|
||||
SnipeAsset::query()->whereKey($items->pluck('snipe_asset_id'))->orderBy('id')->lockForUpdate()->get();
|
||||
@@ -51,13 +53,17 @@ class ApprovalService
|
||||
|
||||
return $locked->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$approved->requester->notify(new ReservationDecisionNotification($approved, 'approved', $comment));
|
||||
|
||||
return $approved;
|
||||
}
|
||||
|
||||
public function reject(Reservation $reservation, User $actor, string $reason): Reservation
|
||||
{
|
||||
$this->authorize($actor);
|
||||
|
||||
return DB::transaction(function () use ($reservation, $actor, $reason): Reservation {
|
||||
$rejected = DB::transaction(function () use ($reservation, $actor, $reason): Reservation {
|
||||
$locked = $this->lockPending($reservation);
|
||||
$this->transition($locked, $actor, ReservationStatus::Rejected, 'rejected', $reason, [
|
||||
'rejected_by' => $actor->id,
|
||||
@@ -70,13 +76,17 @@ class ApprovalService
|
||||
|
||||
return $locked->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$rejected->requester->notify(new ReservationDecisionNotification($rejected, 'rejected', $reason));
|
||||
|
||||
return $rejected;
|
||||
}
|
||||
|
||||
public function requestInformation(Reservation $reservation, User $actor, string $comment): Reservation
|
||||
{
|
||||
$this->authorize($actor);
|
||||
|
||||
return DB::transaction(function () use ($reservation, $actor, $comment): Reservation {
|
||||
$awaitingInformation = DB::transaction(function () use ($reservation, $actor, $comment): Reservation {
|
||||
$locked = $this->lockPending($reservation);
|
||||
$this->transition($locked, $actor, ReservationStatus::AwaitingInformation, 'information_requested', $comment, [
|
||||
'decision_reason' => $comment,
|
||||
@@ -85,6 +95,81 @@ class ApprovalService
|
||||
|
||||
return $locked->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$awaitingInformation->requester->notify(
|
||||
new ReservationDecisionNotification($awaitingInformation, 'information_requested', $comment)
|
||||
);
|
||||
|
||||
return $awaitingInformation;
|
||||
}
|
||||
|
||||
public function substituteAsset(
|
||||
Reservation $reservation,
|
||||
ReservationItem $item,
|
||||
SnipeAsset $replacement,
|
||||
User $actor,
|
||||
string $reason,
|
||||
): Reservation {
|
||||
$this->authorize($actor);
|
||||
|
||||
$updated = DB::transaction(function () use ($reservation, $item, $replacement, $actor, $reason): Reservation {
|
||||
$locked = $this->lockPending($reservation);
|
||||
$lockedItem = ReservationItem::query()->whereKey($item->id)->lockForUpdate()->firstOrFail();
|
||||
if ($lockedItem->reservation_id !== $locked->id || $lockedItem->status !== ReservationItemStatus::Held) {
|
||||
throw ValidationException::withMessages(['replacement_asset_id' => 'O item já não pode ser substituído.']);
|
||||
}
|
||||
|
||||
$assets = SnipeAsset::query()
|
||||
->whereKey([$lockedItem->snipe_asset_id, $replacement->id])
|
||||
->orderBy('id')
|
||||
->lockForUpdate()
|
||||
->get()
|
||||
->keyBy('id');
|
||||
$original = $assets->get($lockedItem->snipe_asset_id);
|
||||
$newAsset = $assets->get($replacement->id);
|
||||
|
||||
if ($original === null || $newAsset === null || ! $this->sameCategory($original, $newAsset)) {
|
||||
throw ValidationException::withMessages(['replacement_asset_id' => 'Selecione um equipamento da mesma categoria.']);
|
||||
}
|
||||
if ($locked->items()->where('snipe_asset_id', $newAsset->id)->exists()
|
||||
|| ! $this->availability->isAvailable($newAsset, $lockedItem->starts_at, $lockedItem->ends_at)) {
|
||||
throw ValidationException::withMessages(['replacement_asset_id' => 'O equipamento substituto não está disponível.']);
|
||||
}
|
||||
|
||||
$lockedItem->update(['status' => ReservationItemStatus::Cancelled]);
|
||||
$locked->items()->create([
|
||||
'snipe_asset_id' => $newAsset->id,
|
||||
'starts_at' => $lockedItem->starts_at,
|
||||
'ends_at' => $lockedItem->ends_at,
|
||||
'status' => ReservationItemStatus::Held,
|
||||
'notes' => "Substitui {$original->asset_tag}: {$reason}",
|
||||
]);
|
||||
ApprovalAction::create([
|
||||
'reservation_id' => $locked->id,
|
||||
'actor_id' => $actor->id,
|
||||
'action' => 'asset_substituted',
|
||||
'previous_status' => $locked->status->value,
|
||||
'new_status' => $locked->status->value,
|
||||
'comment' => $reason,
|
||||
]);
|
||||
AuditLog::create([
|
||||
'actor_id' => $actor->id,
|
||||
'event' => 'reservation.asset_substituted',
|
||||
'auditable_type' => $locked->getMorphClass(),
|
||||
'auditable_id' => $locked->id,
|
||||
'metadata' => [
|
||||
'original_asset_id' => $original->id,
|
||||
'replacement_asset_id' => $newAsset->id,
|
||||
'reason' => $reason,
|
||||
],
|
||||
]);
|
||||
|
||||
return $locked->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$updated->requester->notify(new ReservationDecisionNotification($updated, 'asset_substituted', $reason));
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
private function authorize(User $actor): void
|
||||
@@ -94,6 +179,15 @@ class ApprovalService
|
||||
}
|
||||
}
|
||||
|
||||
private function sameCategory(SnipeAsset $original, SnipeAsset $replacement): bool
|
||||
{
|
||||
if ($original->category_id !== null && $replacement->category_id !== null) {
|
||||
return $original->category_id === $replacement->category_id;
|
||||
}
|
||||
|
||||
return filled($original->category_name) && $original->category_name === $replacement->category_name;
|
||||
}
|
||||
|
||||
private function lockPending(Reservation $reservation): Reservation
|
||||
{
|
||||
$locked = Reservation::query()->whereKey($reservation->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domain\Reservations\Models;
|
||||
|
||||
use App\Domain\Approvals\Models\ApprovalAction;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
@@ -57,4 +58,10 @@ class Reservation extends Model
|
||||
{
|
||||
return $this->hasMany(ReservationItem::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<ApprovalAction, $this> */
|
||||
public function approvalActions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalAction::class)->latest('created_at');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
namespace App\Domain\Reservations\Services;
|
||||
|
||||
use App\Domain\Approvals\Models\ApprovalAction;
|
||||
use App\Domain\Audit\Models\AuditLog;
|
||||
use App\Domain\Identity\Enums\RoleName;
|
||||
use App\Domain\Inventory\Models\SnipeAsset;
|
||||
use App\Domain\Reservations\Enums\ReservationItemStatus;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ReservationSubmittedNotification;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@@ -26,7 +30,7 @@ class ReservationService
|
||||
$startsAt = CarbonImmutable::parse($data['starts_at']);
|
||||
$endsAt = CarbonImmutable::parse($data['ends_at']);
|
||||
|
||||
return DB::transaction(function () use ($requester, $data, $startsAt, $endsAt): Reservation {
|
||||
$reservation = DB::transaction(function () use ($requester, $data, $startsAt, $endsAt): Reservation {
|
||||
$assets = SnipeAsset::query()
|
||||
->whereKey($data['asset_ids'])
|
||||
->lockForUpdate()
|
||||
@@ -75,8 +79,19 @@ class ReservationService
|
||||
'metadata' => ['asset_ids' => $assets->modelKeys()],
|
||||
]);
|
||||
|
||||
return $reservation->load('items.asset');
|
||||
return $reservation->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$approvers = User::query()
|
||||
->where('active', true)
|
||||
->whereHas('roles', fn ($query) => $query->whereIn('name', [
|
||||
RoleName::Approver->value,
|
||||
RoleName::Administrator->value,
|
||||
]))
|
||||
->get();
|
||||
Notification::send($approvers, new ReservationSubmittedNotification($reservation));
|
||||
|
||||
return $reservation;
|
||||
}
|
||||
|
||||
public function cancel(Reservation $reservation, User $actor, string $reason): Reservation
|
||||
@@ -124,6 +139,61 @@ class ReservationService
|
||||
}, 3);
|
||||
}
|
||||
|
||||
public function respondToInformation(Reservation $reservation, User $actor, string $response): Reservation
|
||||
{
|
||||
if ($reservation->requester_id !== $actor->id) {
|
||||
throw new AuthorizationException('Apenas o requisitante pode responder a este pedido.');
|
||||
}
|
||||
|
||||
$updated = DB::transaction(function () use ($reservation, $actor, $response): Reservation {
|
||||
$locked = Reservation::query()->whereKey($reservation->id)->lockForUpdate()->firstOrFail();
|
||||
if (! $locked->status->canTransitionTo(ReservationStatus::PendingApproval)
|
||||
|| $locked->status !== ReservationStatus::AwaitingInformation) {
|
||||
throw ValidationException::withMessages([
|
||||
'response' => 'Esta requisição não aguarda esclarecimentos.',
|
||||
]);
|
||||
}
|
||||
|
||||
$locked->update([
|
||||
'status' => ReservationStatus::PendingApproval,
|
||||
'approval_deadline' => now()->addHours(48),
|
||||
]);
|
||||
ApprovalAction::create([
|
||||
'reservation_id' => $locked->id,
|
||||
'actor_id' => $actor->id,
|
||||
'action' => 'information_provided',
|
||||
'previous_status' => ReservationStatus::AwaitingInformation->value,
|
||||
'new_status' => ReservationStatus::PendingApproval->value,
|
||||
'comment' => $response,
|
||||
]);
|
||||
AuditLog::create([
|
||||
'actor_id' => $actor->id,
|
||||
'event' => 'reservation.information_provided',
|
||||
'auditable_type' => $locked->getMorphClass(),
|
||||
'auditable_id' => $locked->id,
|
||||
'before' => ['status' => ReservationStatus::AwaitingInformation->value],
|
||||
'after' => ['status' => ReservationStatus::PendingApproval->value],
|
||||
'metadata' => ['response' => $response],
|
||||
]);
|
||||
|
||||
return $locked->load('requester', 'items.asset');
|
||||
}, 3);
|
||||
|
||||
$approvers = User::query()
|
||||
->where('active', true)
|
||||
->whereHas('roles', fn ($query) => $query->whereIn('name', [
|
||||
RoleName::Approver->value,
|
||||
RoleName::Administrator->value,
|
||||
]))
|
||||
->get();
|
||||
Notification::send(
|
||||
$approvers,
|
||||
new ReservationSubmittedNotification($updated, 'information_provided'),
|
||||
);
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
public function expirePending(int $limit = 100): int
|
||||
{
|
||||
$reservationIds = Reservation::query()
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Domain\Approvals\Services\ApprovalService;
|
||||
use App\Domain\Inventory\Models\SnipeAsset;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use App\Domain\Reservations\Models\ReservationItem;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -14,20 +17,46 @@ class ApprovalController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filters = $request->validate(['search' => ['nullable', 'string', 'max:100']]);
|
||||
$filters = $request->validate([
|
||||
'search' => ['nullable', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'max:100'],
|
||||
'date_from' => ['nullable', 'date'],
|
||||
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
|
||||
]);
|
||||
|
||||
return Inertia::render('approvals/index', [
|
||||
'reservations' => Reservation::query()
|
||||
->with(['requester:id,name,email,department', 'items.asset:id,name,asset_tag,category_name'])
|
||||
->with([
|
||||
'requester:id,name,email,department',
|
||||
'items.asset:id,name,asset_tag,category_id,category_name',
|
||||
'approvalActions.actor:id,name',
|
||||
])
|
||||
->whereIn('status', [ReservationStatus::PendingApproval, ReservationStatus::AwaitingInformation])
|
||||
->when($filters['search'] ?? null, fn ($query, string $search) => $query
|
||||
->where(fn ($query) => $query
|
||||
->where('reference', 'like', "%{$search}%")
|
||||
->orWhereHas('requester', fn ($query) => $query->where('name', 'like', "%{$search}%"))))
|
||||
->when($filters['category'] ?? null, fn ($query, string $category) => $query
|
||||
->whereHas('items.asset', fn ($query) => $query->where('category_name', $category)))
|
||||
->when($filters['date_from'] ?? null, fn ($query, string $date) => $query
|
||||
->where('ends_at', '>', CarbonImmutable::parse($date)->startOfDay()))
|
||||
->when($filters['date_to'] ?? null, fn ($query, string $date) => $query
|
||||
->where('starts_at', '<', CarbonImmutable::parse($date)->addDay()->startOfDay()))
|
||||
->oldest('starts_at')
|
||||
->paginate(20)
|
||||
->withQueryString(),
|
||||
'filters' => $filters,
|
||||
'categories' => SnipeAsset::query()
|
||||
->whereNotNull('category_name')
|
||||
->distinct()
|
||||
->orderBy('category_name')
|
||||
->pluck('category_name'),
|
||||
'replacementAssets' => SnipeAsset::query()
|
||||
->where('active', true)
|
||||
->where('requestable', true)
|
||||
->whereNull('assigned_id')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'asset_tag', 'category_id', 'category_name']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -54,4 +83,17 @@ class ApprovalController extends Controller
|
||||
|
||||
return back()->with('success', "Pedido de esclarecimentos enviado para {$reservation->reference}.");
|
||||
}
|
||||
|
||||
public function substitute(Request $request, Reservation $reservation, ReservationItem $item, ApprovalService $service): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'replacement_asset_id' => ['required', 'integer', 'exists:snipe_assets,id'],
|
||||
'comment' => ['required', 'string', 'min:5', 'max:1000'],
|
||||
]);
|
||||
/** @var SnipeAsset $replacement */
|
||||
$replacement = SnipeAsset::query()->findOrFail($data['replacement_asset_id']);
|
||||
$service->substituteAsset($reservation, $item, $replacement, $request->user(), $data['comment']);
|
||||
|
||||
return back()->with('success', "Equipamento substituído na requisição {$reservation->reference}.");
|
||||
}
|
||||
}
|
||||
|
||||
35
app/Http/Controllers/NotificationController.php
Normal file
35
app/Http/Controllers/NotificationController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return Inertia::render('notifications/index', [
|
||||
'notifications' => $request->user()->notifications()->latest()->paginate(20),
|
||||
]);
|
||||
}
|
||||
|
||||
public function read(Request $request, string $notification): RedirectResponse
|
||||
{
|
||||
$item = $request->user()->notifications()->whereKey($notification)->firstOrFail();
|
||||
$item->markAsRead();
|
||||
|
||||
$url = is_string($item->data['url'] ?? null) ? $item->data['url'] : '/notificacoes';
|
||||
|
||||
return redirect()->to($url);
|
||||
}
|
||||
|
||||
public function readAll(Request $request): RedirectResponse
|
||||
{
|
||||
$request->user()->unreadNotifications->markAsRead();
|
||||
|
||||
return back()->with('success', 'Notificações marcadas como lidas.');
|
||||
}
|
||||
}
|
||||
@@ -95,4 +95,14 @@ class ReservationController extends Controller
|
||||
|
||||
return back()->with('success', "Requisição {$reservation->reference} cancelada. Os equipamentos ficaram novamente disponíveis.");
|
||||
}
|
||||
|
||||
public function respondToInformation(Request $request, Reservation $reservation, ReservationService $service): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'response' => ['required', 'string', 'min:5', 'max:2000'],
|
||||
]);
|
||||
$service->respondToInformation($reservation, $request->user(), $data['response']);
|
||||
|
||||
return back()->with('success', "Esclarecimentos enviados para a requisição {$reservation->reference}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'administer' => fn (): bool => $request->user()?->can('administer') ?? false,
|
||||
'approveReservations' => fn (): bool => $request->user()?->can('approve-reservations') ?? false,
|
||||
],
|
||||
'unreadNotifications' => fn (): int => $request->user()?->unreadNotifications()->count() ?? 0,
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
|
||||
77
app/Notifications/ReservationDecisionNotification.php
Normal file
77
app/Notifications/ReservationDecisionNotification.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ReservationDecisionNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Reservation $reservation,
|
||||
public readonly string $event,
|
||||
public readonly ?string $comment = null,
|
||||
) {
|
||||
$this->afterCommit();
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$message = (new MailMessage)
|
||||
->subject($this->subject())
|
||||
->greeting("Atualização da requisição {$this->reservation->reference}")
|
||||
->line($this->message());
|
||||
|
||||
if (filled($this->comment)) {
|
||||
$message->line("Comentário: {$this->comment}");
|
||||
}
|
||||
|
||||
return $message->action('Consultar requisições', url('/requisicoes'));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'reservation_uuid' => $this->reservation->uuid,
|
||||
'reference' => $this->reservation->reference,
|
||||
'event' => $this->event,
|
||||
'message' => $this->message(),
|
||||
'comment' => $this->comment,
|
||||
'url' => '/requisicoes',
|
||||
];
|
||||
}
|
||||
|
||||
private function subject(): string
|
||||
{
|
||||
return match ($this->event) {
|
||||
'approved' => "Requisição {$this->reservation->reference} aprovada",
|
||||
'rejected' => "Requisição {$this->reservation->reference} rejeitada",
|
||||
'information_requested' => "Esclarecimentos necessários para {$this->reservation->reference}",
|
||||
'asset_substituted' => "Equipamento alterado em {$this->reservation->reference}",
|
||||
default => "Atualização da requisição {$this->reservation->reference}",
|
||||
};
|
||||
}
|
||||
|
||||
private function message(): string
|
||||
{
|
||||
return match ($this->event) {
|
||||
'approved' => 'A sua requisição foi aprovada.',
|
||||
'rejected' => 'A sua requisição foi rejeitada.',
|
||||
'information_requested' => 'O aprovador pediu informações adicionais.',
|
||||
'asset_substituted' => 'Um equipamento da sua requisição foi substituído por outro equivalente.',
|
||||
default => 'O estado da sua requisição foi atualizado.',
|
||||
};
|
||||
}
|
||||
}
|
||||
55
app/Notifications/ReservationSubmittedNotification.php
Normal file
55
app/Notifications/ReservationSubmittedNotification.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ReservationSubmittedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Reservation $reservation,
|
||||
public readonly string $event = 'submitted',
|
||||
) {
|
||||
$this->afterCommit();
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail', 'database'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->event === 'information_provided'
|
||||
? "Resposta recebida para {$this->reservation->reference}"
|
||||
: "Nova requisição {$this->reservation->reference}")
|
||||
->greeting($this->event === 'information_provided'
|
||||
? 'O requisitante respondeu ao pedido de esclarecimentos'
|
||||
: 'Nova requisição para aprovação')
|
||||
->line("{$this->reservation->requester->name} submeteu a requisição {$this->reservation->reference} para decisão.")
|
||||
->line("Período: {$this->reservation->starts_at->format('d/m/Y H:i')} — {$this->reservation->ends_at->format('d/m/Y H:i')}.")
|
||||
->action('Analisar requisição', url('/aprovacoes'));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'reservation_uuid' => $this->reservation->uuid,
|
||||
'reference' => $this->reservation->reference,
|
||||
'event' => $this->event,
|
||||
'message' => $this->event === 'information_provided'
|
||||
? "Foram prestados esclarecimentos para {$this->reservation->reference}."
|
||||
: "Nova requisição {$this->reservation->reference} aguarda decisão.",
|
||||
'url' => '/aprovacoes',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user