feat: complete administration and reporting
This commit is contained in:
29
app/Domain/Administration/Models/PortalSetting.php
Normal file
29
app/Domain/Administration/Models/PortalSetting.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Administration\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PortalSetting extends Model
|
||||
{
|
||||
public $incrementing = false;
|
||||
|
||||
protected $primaryKey = 'key';
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = ['key', 'value', 'description', 'updated_by'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['value' => 'json'];
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function updatedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
100
app/Domain/Administration/Services/ReservationPolicy.php
Normal file
100
app/Domain/Administration/Services/ReservationPolicy.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Administration\Services;
|
||||
|
||||
use App\Domain\Administration\Models\PortalSetting;
|
||||
use App\Domain\Audit\Models\AuditLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ReservationPolicy
|
||||
{
|
||||
/** @return array<string, array{label:string,description:string,default:int,min:int,max:int,unit:string}> */
|
||||
public function definitions(): array
|
||||
{
|
||||
return [
|
||||
'reservation.min_notice_hours' => [
|
||||
'label' => 'Antecedência mínima',
|
||||
'description' => 'Horas mínimas entre a submissão e o início da utilização.',
|
||||
'default' => 0, 'min' => 0, 'max' => 720, 'unit' => 'horas',
|
||||
],
|
||||
'reservation.max_duration_hours' => [
|
||||
'label' => 'Duração máxima',
|
||||
'description' => 'Duração máxima permitida para uma requisição.',
|
||||
'default' => 168, 'min' => 1, 'max' => 2160, 'unit' => 'horas',
|
||||
],
|
||||
'reservation.turnaround_minutes' => [
|
||||
'label' => 'Margem entre reservas',
|
||||
'description' => 'Tempo bloqueado antes e depois da utilização para preparação e conferência.',
|
||||
'default' => 0, 'min' => 0, 'max' => 1440, 'unit' => 'minutos',
|
||||
],
|
||||
'reservation.approval_deadline_hours' => [
|
||||
'label' => 'Prazo de aprovação',
|
||||
'description' => 'Tempo até uma pré-reserva pendente expirar.',
|
||||
'default' => 48, 'min' => 1, 'max' => 336, 'unit' => 'horas',
|
||||
],
|
||||
'reservation.max_assets' => [
|
||||
'label' => 'Equipamentos por requisição',
|
||||
'description' => 'Número máximo de equipamentos no mesmo pedido.',
|
||||
'default' => 10, 'min' => 1, 'max' => 50, 'unit' => 'equipamentos',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function integer(string $key): int
|
||||
{
|
||||
$definitions = $this->definitions();
|
||||
if (! isset($definitions[$key])) {
|
||||
throw new \InvalidArgumentException("Política desconhecida: {$key}");
|
||||
}
|
||||
|
||||
return $this->values()[$key] ?? $definitions[$key]['default'];
|
||||
}
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function values(): array
|
||||
{
|
||||
return Cache::remember('portal.reservation_policy', now()->addHour(), function (): array {
|
||||
/** @var array<string, int> $stored */
|
||||
$stored = PortalSetting::query()
|
||||
->whereIn('key', array_keys($this->definitions()))
|
||||
->pluck('value', 'key')
|
||||
->map(fn (mixed $value): int => (int) $value)
|
||||
->all();
|
||||
|
||||
return collect($this->definitions())
|
||||
->mapWithKeys(fn (array $definition, string $key): array => [
|
||||
$key => $stored[$key] ?? $definition['default'],
|
||||
])
|
||||
->all();
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, int> $values */
|
||||
public function update(array $values, User $actor): void
|
||||
{
|
||||
DB::transaction(function () use ($values, $actor): void {
|
||||
$before = $this->values();
|
||||
foreach ($this->definitions() as $key => $definition) {
|
||||
PortalSetting::query()->updateOrCreate(
|
||||
['key' => $key],
|
||||
[
|
||||
'value' => $values[$key],
|
||||
'description' => $definition['description'],
|
||||
'updated_by' => $actor->id,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
AuditLog::create([
|
||||
'actor_id' => $actor->id,
|
||||
'event' => 'settings.reservation_policy_updated',
|
||||
'before' => $before,
|
||||
'after' => $values,
|
||||
]);
|
||||
}, 3);
|
||||
|
||||
Cache::forget('portal.reservation_policy');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user