Co nowego: - Podgląd logów w panelu admina (Admin > Logi) — pliki storage/logs/*.log bez potrzeby dostępu do kontenera, z filtrami poziomu/tekstu/liczby wpisów i auto-odświeżaniem. - Filtr „Bez kategorii” w kolejce operatora — izoluje zgłoszenia bez przypisanej kategorii/podkategorii. - Narzędzie importu z Heska: już nie tworzy automatycznie kont klientów dla nieznanych e-maili (pomija takie zgłoszenia zamiast zakładać konto), łączy odpowiedzi/właścicieli zgłoszeń z realnymi kontami operatorów po e-mailu, nowe flagi --assign-operators i --fix-closed-dates do donaprawiania wcześniejszych importów, dedykowany log storage/logs/hesk-import.log. - Poprawka: pulpit statystyk operatora (rozkład wg kategorii/podkategorii i filtr kategorii) pomijał zgłoszenia przypisane do samej kategorii bez podkategorii (np. z poczty IMAP) — teraz liczone poprawnie. - Poprawka: błąd JS i zawieszone w tle liczniki przy nawigacji z widoku z aktywnym licznikiem (najbardziej odczuwalne w liczniku czasu pracy operatora). - Porządki w bazie: usunięte niewykorzystywane kolumny (users.remember_token, users.email_verified_at, email_templates.trigger_label); wartości pól dodatkowych, stan triage/podsumowania AI i powiązany sprzęt Snipe-IT przeniesione z tabeli tickets do osobnych tabel (ticket_field_values, ticket_ai_summaries, ticket_snipeit_assets) — bez zmiany zachowania, ale pola dodatkowe są teraz efektywnie przeszukiwalne; dodane brakujące indeksy na 4 tabelach pivot; tickets.source/ticket_messages.source walidowane względem znanego zestawu wartości. Zaktualizowana dokumentacja: README, CLAUDE.md, ARCHITECTURE.md, CHANGELOG.md, wiki/admin, wiki/operator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
239 lines
7.6 KiB
PHP
239 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
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'])]
|
|
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 [
|
|
'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 recentlyViewedTickets(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(Ticket::class, 'ticket_views')
|
|
->withPivot('viewed_at')
|
|
->orderByPivot('viewed_at', 'desc');
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|