feat: complete administration and reporting

This commit is contained in:
2026-08-04 17:40:52 +01:00
parent d829f34580
commit 7a4ac48aaf
42 changed files with 2595 additions and 76 deletions

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Domain\Administration\Services;
use App\Domain\Administration\Models\PortalSetting;
use App\Domain\Audit\Models\AuditLog;
use App\Domain\Reservations\Models\Reservation;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
final class NotificationTemplateService
{
/** @return array<string, array{label:string,subject:string,message:string}> */
public function definitions(): array
{
return [
'submitted' => [
'label' => 'Nova requisição',
'subject' => 'Nova requisição {reference}',
'message' => '{requester} submeteu {reference} para decisão. Período: {starts_at} — {ends_at}.',
],
'information_provided' => [
'label' => 'Esclarecimentos prestados',
'subject' => 'Resposta recebida para {reference}',
'message' => '{requester} prestou esclarecimentos para {reference}.',
],
'approved' => [
'label' => 'Requisição aprovada',
'subject' => 'Requisição {reference} aprovada',
'message' => 'A sua requisição {reference} foi aprovada.',
],
'rejected' => [
'label' => 'Requisição rejeitada',
'subject' => 'Requisição {reference} rejeitada',
'message' => 'A sua requisição {reference} foi rejeitada.',
],
'information_requested' => [
'label' => 'Pedido de esclarecimentos',
'subject' => 'Esclarecimentos necessários para {reference}',
'message' => 'O aprovador pediu informações adicionais para {reference}.',
],
'asset_substituted' => [
'label' => 'Equipamento substituído',
'subject' => 'Equipamento alterado em {reference}',
'message' => 'Um equipamento de {reference} foi substituído por outro equivalente.',
],
];
}
/** @return array<string, array{subject:string,message:string}> */
public function templates(): array
{
return Cache::remember('portal.notification_templates', now()->addHour(), function (): array {
$keys = [];
foreach (array_keys($this->definitions()) as $event) {
$keys[] = "notification.{$event}.subject";
$keys[] = "notification.{$event}.message";
}
$stored = PortalSetting::query()
->whereIn('key', $keys)
->get(['key', 'value'])
->mapWithKeys(fn (PortalSetting $setting): array => [$setting->key => $setting->value]);
return collect($this->definitions())->mapWithKeys(function (array $definition, string $event) use ($stored): array {
return [$event => [
'subject' => (string) ($stored["notification.{$event}.subject"] ?? $definition['subject']),
'message' => (string) ($stored["notification.{$event}.message"] ?? $definition['message']),
]];
})->all();
});
}
/** @return array{subject:string,message:string} */
public function render(string $event, Reservation $reservation, ?string $comment = null): array
{
$template = $this->templates()[$event] ?? [
'subject' => 'Atualização da requisição {reference}',
'message' => 'O estado da requisição {reference} foi atualizado.',
];
$replacements = [
'{reference}' => $reservation->reference,
'{requester}' => $reservation->requester->name,
'{starts_at}' => $reservation->starts_at->format('d/m/Y H:i'),
'{ends_at}' => $reservation->ends_at->format('d/m/Y H:i'),
'{comment}' => $comment ?? '',
];
return [
'subject' => strtr($template['subject'], $replacements),
'message' => strtr($template['message'], $replacements),
];
}
/** @param array<string, array{subject:string,message:string}> $templates */
public function update(array $templates, User $actor): void
{
DB::transaction(function () use ($templates, $actor): void {
$before = $this->templates();
foreach ($templates as $event => $template) {
foreach (['subject', 'message'] as $field) {
PortalSetting::query()->updateOrCreate(
['key' => "notification.{$event}.{$field}"],
[
'value' => $template[$field],
'description' => "Modelo {$field} para {$event}",
'updated_by' => $actor->id,
],
);
}
}
AuditLog::create([
'actor_id' => $actor->id,
'event' => 'settings.notification_templates_updated',
'before' => $before,
'after' => $templates,
]);
}, 3);
Cache::forget('portal.notification_templates');
}
}