Files
servicedesk/src/app/Models/User.php
Kacper ab90abcaa3 v1.1.3
- Triggers (Admin > Wyzwalacze): event-driven rules that fire immediately on
  a ticket lifecycle event (created/updated/status/priority/assignee/team/
  category changed, new reply), with AND-conditions and ordered actions
  (set status/priority/team/assignee, send e-mail). Ships its own dedicated,
  freely add/edit/delete-able e-mail templates, kept separate from the fixed
  system templates.
- Ticket watching: operators can star/"Obserwuj" any ticket to follow it
  regardless of assignment/team.
- Real-time notification bell (private per-user broadcast channel, 30s
  fallback poll) with an opt-in in-tab browser push notification.
- Per-user notification preferences (/settings/notifications): scope
  (mine/unassigned/watched/all) and e-mail toggle per event category.
- Admin > Integracje: new tab for LDAP/AD + BookStack config, split out of
  Konfiguracja.
- Operator queue: Podkategoria/Zespół/Utworzono columns (off by default).
- Obserwuj button moved next to the auto-refresh countdown; trigger
  condition builder shows subcategory/zgłaszający as name dropdowns instead
  of raw IDs; /settings/notifications got a back link, full-width push
  card, and a bordered table container; admin panel tab and operator queue
  view now persist across a plain page refresh.
- Docs: README/ARCHITECTURE/wiki updated for all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 23:43:01 +02:00

234 lines
7.4 KiB
PHP

<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use LdapRecord\Laravel\Auth\AuthenticatesWithLdap;
use LdapRecord\Laravel\Auth\LdapAuthenticatable;
#[Fillable(['name', 'email', 'password', 'roles', 'custom_field_values'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable implements LdapAuthenticatable
{
/** @use HasFactory<UserFactory> */
use AuthenticatesWithLdap, HasFactory, Notifiable;
/**
* Pending values for the "roles" / "custom_field_values" virtual
* attributes (see getAttribute()/setAttribute() below) — neither is a
* real column anymore, so a plain write can't land in $attributes; it's
* stashed here until saved() flushes it into role_user / user_field_values.
*/
protected ?array $pendingRoleKeys = null;
protected ?array $pendingFieldValues = null;
protected static function booted(): void
{
static::saved(function (User $user) {
if ($user->pendingRoleKeys !== null) {
$ids = Role::query()->whereIn('key', $user->pendingRoleKeys)->pluck('id');
$user->roleAssignments()->sync($ids);
$user->unsetRelation('roleAssignments');
$user->pendingRoleKeys = null;
}
if ($user->pendingFieldValues !== null) {
$user->syncFieldValues($user->pendingFieldValues);
$user->unsetRelation('fieldValues');
$user->pendingFieldValues = null;
}
});
}
/**
* Get an attribute's value. Overridden (rather than using an Eloquent
* "Attribute" get/set mutator) because "roles" and "custom_field_values"
* no longer back a real column at all — there's nothing for the
* mutator's return value to be written into.
*/
public function getAttribute($key)
{
if ($key === 'roles') {
return $this->pendingRoleKeys ?? $this->roleAssignments->pluck('key')->all();
}
if ($key === 'custom_field_values') {
return $this->pendingFieldValues ?? $this->fieldValues->pluck('value', 'user_field_id')->all();
}
return parent::getAttribute($key);
}
public function setAttribute($key, $value)
{
if ($key === 'roles') {
$this->pendingRoleKeys = $value ?? [];
return $this;
}
if ($key === 'custom_field_values') {
$this->pendingFieldValues = $value ?? [];
return $this;
}
return parent::setAttribute($key, $value);
}
protected function syncFieldValues(array $values): void
{
$keep = [];
foreach ($values as $fieldId => $value) {
if ($value === null || $value === '') {
continue;
}
$keep[] = $fieldId;
UserFieldValue::query()->updateOrCreate(
['user_id' => $this->id, 'user_field_id' => $fieldId],
['value' => is_bool($value) ? ($value ? '1' : '0') : (string) $value]
);
}
UserFieldValue::query()->where('user_id', $this->id)->whereNotIn('user_field_id', $keep)->delete();
}
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* The roles this user actually holds, stored via the role_user pivot —
* see the roles virtual attribute above for the plain-array reader/writer
* that everything else in the app uses instead of this relation directly.
*/
public function roleAssignments(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
/**
* Filters to users holding the given role — replaces the old
* whereJsonContains('roles', $key) now that roles live in role_user.
*/
public function scopeWithRole(Builder $query, string $roleKey): Builder
{
return $query->whereHas('roleAssignments', fn (Builder $q) => $q->where('key', $roleKey));
}
/**
* A user can hold several roles at once (e.g. operator + admin) — each
* grants access to its matching area independently.
*/
public function hasRole(string $role): bool
{
return in_array($role, $this->roles ?? [], true);
}
public function isClient(): bool
{
return $this->hasRole('client');
}
public function isOperator(): bool
{
return $this->hasRole('operator');
}
public function isAdmin(): bool
{
return $this->hasRole('admin');
}
/**
* Where to land a user with (possibly) several roles right after login —
* always the client area, even for admins/operators, since every account
* carries the "client" role by default (see AssignDefaultRole) and staff
* switch into their other areas afterwards via the role switcher in the
* header rather than landing there automatically.
*/
public function defaultArea(): string
{
return match (true) {
$this->isClient() => '/client',
$this->isAdmin() => '/admin',
$this->isOperator() => '/operator',
default => '/client',
};
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class, 'team_user');
}
public function ticketsAsCustomer(): HasMany
{
return $this->hasMany(Ticket::class, 'customer_id');
}
public function watchedTickets(): BelongsToMany
{
return $this->belongsToMany(Ticket::class, 'ticket_watchers');
}
public function ticketsAssigned(): HasMany
{
return $this->hasMany(Ticket::class, 'assignee_id');
}
public function savedQueueViews(): HasMany
{
return $this->hasMany(SavedQueueView::class);
}
/**
* The admin-defined "user field" values, stored one row per field in
* user_field_values — see the custom_field_values virtual attribute
* above for the plain array-by-field-id reader/writer everything else
* in the app uses instead of this relation directly.
*/
public function fieldValues(): HasMany
{
return $this->hasMany(UserFieldValue::class);
}
/**
* The admin-defined "user field" values that actually have something in
* them, label + display value, for showing next to a reporter/user
* elsewhere in the app (mirrors Ticket::customFieldEntries()).
*/
public function customFieldEntries(): array
{
$values = $this->custom_field_values ?? [];
return UserField::query()->orderBy('sort_order')->get()
->filter(fn (UserField $field) => array_key_exists($field->id, $values) && $values[$field->id] !== '' && $values[$field->id] !== null)
->map(fn (UserField $field) => [
'label' => $field->label,
'value' => $field->type === 'checkbox'
? ($values[$field->id] ? 'Tak' : 'Nie')
: (string) $values[$field->id],
])
->values()
->all();
}
}