- Snipe-IT asset inventory integration (Admin > Integracje), optional and off by default: connect by API address + personal token (+ SSL-verification bypass). Three independent toggles: client can pick which of their own Snipe-IT assets a ticket concerns (scoped to admin-selected subcategories, empty = never shows), operator sees the requester's assets in a ticket-view sidebar, operator can search the whole inventory from that same sidebar (not a separate page) for shared equipment. Assets shown as "numer środka - numer seryjny - producent model" + category; a linked asset's live status is fetched fresh on the ticket page, and unlinking stays available to an operator even with both view/search toggles off. - AI summary: a "Wygeneruj teraz" button for an immediate on-demand refresh, plus a new admin toggle to regenerate right after every new reply/note instead of only on the next scheduled sweep. The transcript sent to the model now also includes the ticket's own opening body, fixing summaries missing the original request on long threads. - Fixed: the status dropdown in the operator ticket view could keep showing the pre-change status after a status-changing quick action until the next page load (Livewire/Alpine-morph quirk for wire:change-bound selects). - Docs: README/ARCHITECTURE/CHANGELOG/install/wiki updated for all of the above, including correcting the AI-summary refresh description left over from the 1.2.1 release notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1397 lines
112 KiB
PHP
1397 lines
112 KiB
PHP
@php
|
|
$tabGroups = [
|
|
'Zgłoszenia' => [
|
|
['key' => 'categories', 'label' => 'Kategorie', 'icon' => 'category'],
|
|
['key' => 'fields', 'label' => 'Pola dodatkowe', 'icon' => 'list_alt'],
|
|
['key' => 'statuses', 'label' => 'Statusy', 'icon' => 'flag'],
|
|
['key' => 'priorities', 'label' => 'Priorytety i SLA', 'icon' => 'priority_high'],
|
|
['key' => 'reply-quick-actions', 'label' => 'Szybkie akcje odpowiedzi', 'icon' => 'bolt'],
|
|
['key' => 'response-templates', 'label' => 'Szablony odpowiedzi', 'icon' => 'chat'],
|
|
['key' => 'automation-rules', 'label' => 'Automatyzacja SLA', 'icon' => 'bolt'],
|
|
['key' => 'triggers', 'label' => 'Wyzwalacze', 'icon' => 'rule'],
|
|
],
|
|
'Zespół' => [
|
|
['key' => 'users', 'label' => 'Użytkownicy', 'icon' => 'group'],
|
|
['key' => 'teams', 'label' => 'Zespoły', 'icon' => 'groups'],
|
|
['key' => 'user-fields', 'label' => 'Pola dodatkowe', 'icon' => 'badge'],
|
|
],
|
|
'Ustawienia' => [
|
|
['key' => 'templates', 'label' => 'Szablony e-mail', 'icon' => 'mail'],
|
|
['key' => 'email', 'label' => 'Poczta', 'icon' => 'forward_to_inbox'],
|
|
['key' => 'branding', 'label' => 'Wygląd i branding', 'icon' => 'palette'],
|
|
['key' => 'config', 'label' => 'Konfiguracja', 'icon' => 'settings'],
|
|
['key' => 'integrations', 'label' => 'Integracje', 'icon' => 'hub'],
|
|
['key' => 'api-keys', 'label' => 'Klucze API', 'icon' => 'vpn_key'],
|
|
['key' => 'about', 'label' => 'O aplikacji', 'icon' => 'info'],
|
|
],
|
|
];
|
|
@endphp
|
|
<div style="flex:1;display:flex;flex-direction:column">
|
|
<x-topbar />
|
|
|
|
<div class="app-split" style="flex:1;display:flex;min-height:0">
|
|
<div class="side-rail" style="padding:16px 10px;gap:16px;overflow:auto;background:color-mix(in srgb, var(--color-text) 5%, var(--color-bg))">
|
|
@foreach ($tabGroups as $groupLabel => $items)
|
|
<div style="display:flex;flex-direction:column;gap:1px">
|
|
<div style="font-size:10px;letter-spacing:0.09em;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 45%, transparent);padding:4px 12px 6px;white-space:nowrap">{{ $groupLabel }}</div>
|
|
@foreach ($items as $item)
|
|
<button type="button" wire:click="setTab('{{ $item['key'] }}')" style="display:flex;align-items:center;gap:10px;width:100%;padding:8px 12px;border-radius:6px;border:none;cursor:pointer;font-size:13.5px;text-align:left;white-space:nowrap;background:{{ $tab === $item['key'] ? 'color-mix(in srgb, var(--color-accent) 14%, transparent)' : 'transparent' }};color:{{ $tab === $item['key'] ? 'var(--color-accent)' : 'var(--color-text)' }}">
|
|
<span class="material-symbols-outlined" style="font-size:18px">{{ $item['icon'] }}</span>
|
|
<span>{{ $item['label'] }}</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="page-pad" style="flex:1;min-width:0;padding:24px 28px;overflow:auto">
|
|
|
|
{{-- ================= CATEGORIES ================= --}}
|
|
@if ($tab === 'categories')
|
|
<h3 style="margin:0 0 16px">Kategorie i podkategorie</h3>
|
|
<form wire:submit="addCategory" style="display:flex;gap:10px;margin-bottom:20px">
|
|
<input class="input" placeholder="Nazwa nowej kategorii" wire:model="newCategoryName" style="flex:1;max-width:300px">
|
|
<button class="btn btn-secondary" type="submit">Dodaj kategorię</button>
|
|
</form>
|
|
|
|
<div style="display:flex;flex-direction:column;gap:20px">
|
|
@foreach ($this->categories as $cat)
|
|
<div style="border:1px solid var(--color-divider);border-radius:8px;overflow:hidden">
|
|
<div style="background:color-mix(in srgb, var(--color-text) 5%, var(--color-bg));padding:12px 16px;display:flex;align-items:flex-start;gap:10px;border-bottom:1px solid var(--color-divider)">
|
|
<span class="material-symbols-outlined" style="font-size:18px;color:var(--color-accent);margin-top:6px">category</span>
|
|
@if ($editingCategoryId === $cat->id)
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:6px">
|
|
<input class="input" wire:model="editingCategoryName" placeholder="Nazwa">
|
|
<textarea class="input" wire:model="editingCategoryDescription" placeholder="Opis (opcjonalnie)" style="min-height:60px"></textarea>
|
|
</div>
|
|
<div style="display:flex;gap:6px;flex:none;margin-top:6px">
|
|
<button type="button" class="btn btn-ghost" wire:click="saveEditCategory">Zapisz</button>
|
|
<button type="button" class="btn btn-ghost" wire:click="cancelEditCategory">Anuluj</button>
|
|
</div>
|
|
@else
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:2px;padding-top:2px">
|
|
<h4 style="margin:0;font-size:14px;font-weight:500">{{ $cat->name }}</h4>
|
|
@if ($cat->description)
|
|
<p class="text-muted" style="margin:0;font-size:12px">{{ $cat->description }}</p>
|
|
@endif
|
|
</div>
|
|
<div style="display:flex;gap:6px;flex:none">
|
|
<button class="btn btn-icon" type="button" wire:click="startEditCategory({{ $cat->id }}, @js($cat->name), @js($cat->description))" style="padding:4px;width:28px;height:28px"><span class="material-symbols-outlined" style="font-size:16px">edit</span></button>
|
|
<button class="btn btn-icon" type="button" wire:click="removeCategory({{ $cat->id }})" style="padding:4px;width:28px;height:28px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:16px">delete</span></button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table class="table" style="margin:0;border-top:none">
|
|
<thead><tr><th>Podkategoria</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($cat->subcategories as $i => $sub)
|
|
<tr>
|
|
<td>
|
|
<div style="display:flex;flex-direction:column;gap:2px">
|
|
<div style="display:flex;align-items:center;gap:8px;white-space:nowrap"><span class="material-symbols-outlined" style="font-size:16px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">folder</span>{{ $sub->name }}</div>
|
|
@if ($sub->description)
|
|
<div style="font-size:12px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{ $sub->description }}</div>
|
|
@endif
|
|
<div style="font-size:12px;color:color-mix(in srgb, var(--color-text) 55%, transparent);white-space:nowrap">Pola: {{ $sub->customFields->pluck('label')->implode(', ') ?: '—' }}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px;justify-content:flex-end;align-items:center">
|
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryUp({{ $sub->id }})" @disabled($i === 0) style="padding:2px;width:24px;height:24px" title="Przesuń wyżej"><span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span></button>
|
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryDown({{ $sub->id }})" @disabled($i === $cat->subcategories->count() - 1) style="padding:2px;width:24px;height:24px" title="Przesuń niżej"><span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span></button>
|
|
<button class="btn btn-ghost" type="button" wire:click="openSubcategoryEditForm({{ $sub->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeSubcategory({{ $sub->id }})">Usuń</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
<tr><td colspan="2">
|
|
<form wire:submit="addSubcategory({{ $cat->id }})" style="display:flex;gap:8px;padding:8px">
|
|
<input class="input" placeholder="Nazwa nowej podkategorii" wire:model="newSubNames.{{ $cat->id }}" style="flex:1;min-width:140px">
|
|
<button class="btn btn-secondary" type="submit" style="white-space:nowrap">Dodaj</button>
|
|
</form>
|
|
</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= FIELDS ================= --}}
|
|
@if ($tab === 'fields')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Pola dodatkowe</h3>
|
|
<button class="btn btn-primary" wire:click="openFieldForm">+ Dodaj pole</button>
|
|
</div>
|
|
@if ($this->customFields->isNotEmpty())
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead><tr><th>Etykieta</th><th>Typ</th><th>Wymagane</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->customFields as $f)
|
|
<tr>
|
|
<td style="white-space:nowrap">{{ $f->label }}</td>
|
|
<td><span class="tag tag-outline">{{ $f->typeLabel() }}</span></td>
|
|
<td>{{ $f->required ? 'Tak' : 'Nie' }}</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px">
|
|
<button class="btn btn-ghost" type="button" wire:click="editField({{ $f->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeField({{ $f->id }})">Usuń</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:13px">Brak pól dodatkowych. Dodaj pierwsze pole używając przycisku wyżej.</p>
|
|
@endif
|
|
@endif
|
|
|
|
{{-- ================= USERS ================= --}}
|
|
@if ($tab === 'users')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Użytkownicy</h3>
|
|
<div style="display:flex;gap:8px">
|
|
<button class="btn btn-secondary" wire:click="syncUsersWithLdap" wire:loading.attr="disabled" wire:target="syncUsersWithLdap">
|
|
<span wire:loading.remove wire:target="syncUsersWithLdap">Synchronizuj z LDAP</span>
|
|
<span wire:loading wire:target="syncUsersWithLdap">Synchronizowanie…</span>
|
|
</button>
|
|
<button class="btn btn-primary" wire:click="openUserForm">+ Zaproś użytkownika</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 12px">Logowanie odbywa się przez LDAP/AD. Zaproszenie tworzy tu tylko rolę i przypisanie — po pierwszym logowaniu tożsamość LDAP zwiąże się automatycznie z tym adresem e-mail. „Synchronizuj z LDAP” pobiera od razu wartości pól dodatkowych (zakładka Pola dodatkowe) dla wszystkich istniejących użytkowników.</p>
|
|
@if ($ldapSyncResult)
|
|
<div class="card" style="padding:10px 14px;margin-bottom:12px;color:var(--color-success);font-size:13px">{{ $ldapSyncResult }}</div>
|
|
@endif
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead><tr><th>Imię i nazwisko</th><th>E-mail</th><th>Rola</th><th>Zespoły</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->users as $u)
|
|
<tr>
|
|
<td style="white-space:nowrap">{{ $u->name }}</td>
|
|
<td style="white-space:nowrap">{{ $u->email }}</td>
|
|
<td>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked($u->hasRole('client')) wire:click="toggleUserRole({{ $u->id }}, 'client')" style="position:static;opacity:1;width:auto;height:auto">Klient</label>
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked($u->hasRole('operator')) wire:click="toggleUserRole({{ $u->id }}, 'operator')" style="position:static;opacity:1;width:auto;height:auto">Operator</label>
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked($u->hasRole('admin')) wire:click="toggleUserRole({{ $u->id }}, 'admin')" style="position:static;opacity:1;width:auto;height:auto">Administrator</label>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
@forelse ($u->teams as $tm)
|
|
<span class="tag tag-outline">{{ $tm->name }}</span>
|
|
@empty
|
|
—
|
|
@endforelse
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px;justify-content:flex-end">
|
|
<button class="btn btn-ghost" type="button" wire:click="viewUser({{ $u->id }})">Zobacz</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="editUser({{ $u->id }})">Edytuj</button>
|
|
@if ($u->id !== auth()->id())
|
|
<button class="btn btn-ghost" type="button" wire:click="removeUser({{ $u->id }})">Usuń</button>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= TEAMS ================= --}}
|
|
@if ($tab === 'teams')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Zespoły</h3>
|
|
<button class="btn btn-primary" wire:click="openTeamForm">+ Nowy zespół</button>
|
|
</div>
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|
@foreach ($this->teams as $tm)
|
|
<div class="card" style="padding:16px;gap:6px">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
|
<span class="card-title">{{ $tm->name }}</span>
|
|
<div style="display:flex;gap:6px;flex:none">
|
|
<button class="btn btn-ghost" type="button" wire:click="editTeam({{ $tm->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeTeam({{ $tm->id }})">Usuń</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-meta">Członkowie: {{ $tm->members->pluck('name')->implode(', ') ?: '—' }}</div>
|
|
<div class="card-meta">Podkategorie: {{ $tm->subcategories->map(fn($s) => $s->category->name.' / '.$s->name)->implode(', ') ?: '—' }}</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= USER FIELDS ================= --}}
|
|
@if ($tab === 'user-fields')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Pola dodatkowe (użytkownicy)</h3>
|
|
<button class="btn btn-primary" wire:click="openUserFieldForm">+ Dodaj pole</button>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Te pola pojawiają się przy edycji użytkownika (nie przy zapraszaniu nowego). Jeśli podasz atrybut LDAP, wartość pobierze się automatycznie przy logowaniu oraz przyciskiem „Synchronizuj z LDAP” w zakładce Użytkownicy.</p>
|
|
@if ($this->userFields->isNotEmpty())
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead><tr><th>Nazwa</th><th>Opis</th><th>Typ</th><th>Atrybut LDAP</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->userFields as $f)
|
|
<tr>
|
|
<td style="white-space:nowrap">{{ $f->label }}</td>
|
|
<td class="text-muted">{{ $f->description ?: '—' }}</td>
|
|
<td><span class="tag tag-outline">{{ $f->typeLabel() }}</span></td>
|
|
<td>{{ $f->ldap_attribute ?: '—' }}</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px">
|
|
<button class="btn btn-ghost" type="button" wire:click="editUserField({{ $f->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeUserField({{ $f->id }})">Usuń</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:13px">Brak pól dodatkowych. Dodaj pierwsze pole używając przycisku wyżej.</p>
|
|
@endif
|
|
@endif
|
|
|
|
{{-- ================= REPLY QUICK ACTIONS ================= --}}
|
|
@if ($tab === 'reply-quick-actions')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Szybkie akcje odpowiedzi</h3>
|
|
<button class="btn btn-primary" wire:click="openReplyQuickActionForm">+ Dodaj akcję</button>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Te opcje pojawiają się w menu obok przycisku „Wyślij odpowiedź” w panelu operatora.</p>
|
|
@if ($this->replyQuickActions->isNotEmpty())
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead><tr><th>Nazwa przycisku</th><th>Zmienia status na</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->replyQuickActions as $qa)
|
|
<tr>
|
|
<td style="white-space:nowrap">{{ $qa->label }}</td>
|
|
<td style="white-space:nowrap">
|
|
@if ($qa->status_key)
|
|
<span style="{{ \App\Models\Ticket::tagStyleFromColor(\App\Models\Status::colorFor($qa->status_key)) }}">{{ $qa->statusLabel() }}</span>
|
|
@else
|
|
<span class="tag tag-outline">Nie zmieniaj</span>
|
|
@endif
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px">
|
|
<button class="btn btn-ghost" type="button" wire:click="editReplyQuickAction({{ $qa->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeReplyQuickAction({{ $qa->id }})">Usuń</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:13px">Brak szybkich akcji. Dodaj pierwszą używając przycisku wyżej.</p>
|
|
@endif
|
|
@endif
|
|
|
|
{{-- ================= RESPONSE TEMPLATES ================= --}}
|
|
@if ($tab === 'response-templates')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Szablony odpowiedzi</h3>
|
|
<button class="btn btn-primary" wire:click="openResponseTemplateForm">+ Dodaj szablon</button>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Te szablony operator może wstawić do treści odpowiedzi z listy „Wstaw szablon odpowiedzi…” w widoku zgłoszenia.</p>
|
|
@if ($this->responseTemplates->isNotEmpty())
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|
@foreach ($this->responseTemplates as $rt)
|
|
<div class="card" style="padding:14px 16px;gap:6px">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
|
<span class="card-title">{{ $rt->label }}</span>
|
|
<div style="display:flex;gap:6px;flex:none">
|
|
<button class="btn btn-ghost" type="button" wire:click="editResponseTemplate({{ $rt->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeResponseTemplate({{ $rt->id }})">Usuń</button>
|
|
</div>
|
|
</div>
|
|
<p class="card-body" style="margin:0;white-space:pre-wrap">{{ \Illuminate\Support\Str::limit($rt->body, 200) }}</p>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:13px">Brak szablonów odpowiedzi. Dodaj pierwszy używając przycisku wyżej.</p>
|
|
@endif
|
|
@endif
|
|
|
|
{{-- ================= AUTOMATION RULES ================= --}}
|
|
@if ($tab === 'automation-rules')
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
<h3 style="margin:0">Automatyzacja SLA</h3>
|
|
<button class="btn btn-primary" wire:click="openAutomationRuleForm">+ Dodaj regułę</button>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Co 15 minut sprawdzane jest, czy zgłoszenie milczy (brak odpowiedzi klienta) dłużej niż próg reguły — jeśli tak (i pasuje do opcjonalnego zawężenia), wykonywana jest wybrana akcja. Reguła nie powtarza się dla tego samego zgłoszenia, dopóki klient znów nie napisze albo zgłoszenie nie zostanie zamknięte i otwarte ponownie.</p>
|
|
@if ($this->automationRules->isNotEmpty())
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|
@foreach ($this->automationRules as $rule)
|
|
<div class="card" style="padding:14px 16px;gap:6px">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
|
<div>
|
|
<span class="card-title">{{ $rule->label }}</span>
|
|
<span class="text-muted" style="font-size:12px;display:block">Brak odpowiedzi klienta ≥ {{ $rule->condition_minutes }} min. → {{ match($rule->action_type) { 'change_priority' => 'zmień priorytet', 'change_status' => 'zmień status', 'change_team' => 'zmień zespół', 'change_assignee' => 'zmień przypisanie', default => $rule->action_type } }}</span>
|
|
</div>
|
|
<div style="display:flex;gap:6px;flex:none;align-items:center">
|
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px">
|
|
<input type="checkbox" wire:click="toggleAutomationRuleEnabled({{ $rule->id }})" @checked($rule->enabled)>
|
|
Aktywna
|
|
</label>
|
|
<button class="btn btn-ghost" type="button" wire:click="editAutomationRule({{ $rule->id }})">Edytuj</button>
|
|
<button class="btn btn-ghost" type="button" wire:click="removeAutomationRule({{ $rule->id }})">Usuń</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:13px">Brak reguł automatyzacji. Dodaj pierwszą używając przycisku wyżej.</p>
|
|
@endif
|
|
@endif
|
|
|
|
{{-- ================= TRIGGERS ================= --}}
|
|
@if ($tab === 'triggers')
|
|
<livewire:admin.triggers />
|
|
@endif
|
|
|
|
{{-- ================= STATUSES ================= --}}
|
|
@if ($tab === 'statuses')
|
|
<h3 style="margin:0 0 6px">Statusy</h3>
|
|
<p class="text-muted" style="font-size:13px;margin:0 0 16px">Nowy, Otwarty i Zamknięty to 3 stałe grupy (nie da się ich usunąć). Pozostałe statusy to tylko dodatkowe warianty grupy „Otwarty” — możesz je dowolnie edytować, usuwać i dodawać nowe.</p>
|
|
<div class="table-wrap" style="margin-bottom:14px">
|
|
<table class="table">
|
|
<thead><tr><th>Grupa</th><th>Nazwa</th><th>Kolor</th><th>Podgląd</th><th></th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->statuses as $i => $s)
|
|
<tr>
|
|
<td><span class="tag tag-outline">{{ match ($s->stage) { 'new' => 'Nowy', 'closed' => 'Zamknięty', default => 'Otwarty' } }}</span></td>
|
|
<td style="min-width:160px"><input class="input" value="{{ $s->label }}" wire:change="setStatusLabel('{{ $s->key }}', $event.target.value)"></td>
|
|
<td><input type="color" style="width:40px;height:34px;border:1px solid var(--color-divider);border-radius:6px;background:transparent;cursor:pointer;padding:2px" value="{{ $s->color }}" wire:change="setStatusColor('{{ $s->key }}', $event.target.value)"></td>
|
|
<td style="white-space:nowrap"><span style="{{ \App\Models\Ticket::tagStyleFromColor($s->color) }}">{{ $s->label }}</span></td>
|
|
<td>
|
|
<div style="display:flex;gap:2px">
|
|
<button class="btn btn-icon" type="button" wire:click="moveStatusUp('{{ $s->key }}')" @disabled($i === 0) style="padding:2px;width:26px;height:26px"><span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span></button>
|
|
<button class="btn btn-icon" type="button" wire:click="moveStatusDown('{{ $s->key }}')" @disabled($i === $this->statuses->count() - 1) style="padding:2px;width:26px;height:26px"><span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span></button>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
@unless ($s->locked)
|
|
<button type="button" class="btn btn-ghost" wire:click="removeStatus('{{ $s->key }}')">Usuń</button>
|
|
@endunless
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:10px;max-width:760px;flex-wrap:wrap">
|
|
<input class="input" style="flex:2;min-width:180px" placeholder="Nazwa nowego statusu (grupa: Otwarty)" wire:model="newStatusLabel">
|
|
<input class="input" style="flex:1;min-width:120px" placeholder="Klucz (opcjonalnie)" wire:model="newStatusKey">
|
|
<input class="input" type="number" style="width:110px" placeholder="Kolejność" wire:model="newStatusSortOrder">
|
|
<input type="color" style="width:40px;height:34px;border:1px solid var(--color-divider);border-radius:6px;background:transparent;cursor:pointer;padding:2px" wire:model="newStatusColor">
|
|
<button type="button" class="btn btn-secondary" wire:click="addStatus">Dodaj status</button>
|
|
</div>
|
|
@error('newStatusKey') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
@endif
|
|
|
|
{{-- ================= PRIORITIES ================= --}}
|
|
@if ($tab === 'priorities')
|
|
<h3 style="margin:0 0 6px">Priorytety i SLA</h3>
|
|
<p class="text-muted" style="font-size:13px;margin:0 0 16px">Edytuj nazwy, kolory i czasy SLA (w minutach), dodawaj i usuwaj wartości — obowiązuje we wszystkich panelach. Czas rozwiązania równy 0 oznacza brak SLA dla danego priorytetu — zgłoszenie nigdy nie zostanie oznaczone jako przeterminowane.</p>
|
|
<div class="table-wrap" style="margin-bottom:14px">
|
|
<table class="table">
|
|
<thead><tr><th>Priorytet</th><th>Kolor</th><th>Podgląd</th><th>Czas odpowiedzi (min)</th><th>Czas rozwiązania (min)</th><th></th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->priorities as $i => $p)
|
|
<tr>
|
|
<td style="min-width:160px"><input class="input" value="{{ $p->label }}" wire:change="setPriorityLabel('{{ $p->key }}', $event.target.value)"></td>
|
|
<td><input type="color" style="width:40px;height:34px;border:1px solid var(--color-divider);border-radius:6px;background:transparent;cursor:pointer;padding:2px" value="{{ $p->color }}" wire:change="setPriorityColor('{{ $p->key }}', $event.target.value)"></td>
|
|
<td style="white-space:nowrap"><span style="{{ \App\Models\Ticket::tagStyleFromColor($p->color) }}">{{ $p->label }}</span></td>
|
|
<td><input class="input" style="width:100px" type="number" value="{{ $p->slaRule?->response_mins ?? 60 }}" wire:change="setSlaResponse('{{ $p->key }}', $event.target.value)"></td>
|
|
<td><input class="input" style="width:100px" type="number" value="{{ $p->slaRule?->resolution_mins ?? 480 }}" wire:change="setSlaResolution('{{ $p->key }}', $event.target.value)"></td>
|
|
<td>
|
|
<div style="display:flex;gap:2px">
|
|
<button class="btn btn-icon" type="button" wire:click="movePriorityUp('{{ $p->key }}')" @disabled($i === 0) style="padding:2px;width:26px;height:26px"><span class="material-symbols-outlined" style="font-size:16px">arrow_upward</span></button>
|
|
<button class="btn btn-icon" type="button" wire:click="movePriorityDown('{{ $p->key }}')" @disabled($i === $this->priorities->count() - 1) style="padding:2px;width:26px;height:26px"><span class="material-symbols-outlined" style="font-size:16px">arrow_downward</span></button>
|
|
</div>
|
|
</td>
|
|
<td><button type="button" class="btn btn-ghost" wire:click="removePriority('{{ $p->key }}')">Usuń</button></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:10px;max-width:760px;flex-wrap:wrap">
|
|
<input class="input" style="flex:2;min-width:180px" placeholder="Nazwa nowego priorytetu" wire:model="newPriorityLabel">
|
|
<input class="input" style="flex:1;min-width:120px" placeholder="Klucz (opcjonalnie)" wire:model="newPriorityKey">
|
|
<input class="input" type="number" style="width:110px" placeholder="Kolejność" wire:model="newPrioritySortOrder">
|
|
<input type="color" style="width:40px;height:34px;border:1px solid var(--color-divider);border-radius:6px;background:transparent;cursor:pointer;padding:2px" wire:model="newPriorityColor">
|
|
<button type="button" class="btn btn-secondary" wire:click="addPriority">Dodaj priorytet</button>
|
|
</div>
|
|
@error('newPriorityKey') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
@endif
|
|
|
|
{{-- ================= EMAIL TEMPLATES ================= --}}
|
|
@if ($tab === 'templates')
|
|
<h3 style="margin:0 0 6px">Powiadomienia e-mail</h3>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Każde zdarzenie ma stały, przypisany na stałe szablon — możesz go dowolnie edytować, ale nie zmienić na inny. Wyłącz przełącznik, żeby dana wiadomość nigdy nie była wysyłana.</p>
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead><tr><th>Zdarzenie</th><th>Odbiorca</th><th>Wysyłane</th><th></th></tr></thead>
|
|
<tbody>
|
|
@foreach ($this->notificationSettings as $ns)
|
|
<tr>
|
|
<td style="white-space:nowrap">{{ $ns->trigger_label }}</td>
|
|
<td><span class="tag tag-outline">{{ $ns->recipient === 'operator' ? 'Operator' : 'Zgłaszający' }}</span></td>
|
|
<td><input type="checkbox" @checked($ns->enabled) wire:click="toggleNotificationEnabled({{ $ns->id }})"></td>
|
|
<td><button type="button" class="btn btn-ghost" wire:click="editEmailTemplate({{ $ns->email_template_id }})">Edytuj szablon</button></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= E-MAIL ================= --}}
|
|
@if ($tab === 'email')
|
|
<h3 style="margin:0 0 6px">Wygląd wiadomości e-mail</h3>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0 0 14px">Każde powiadomienie wysyłane jest w stałym „pudełku” (nazwa firmy, ramka, treść zdarzenia) — tu edytujesz tylko jego stopkę. Po prawej — podgląd na żywo na przykładowym zgłoszeniu.</p>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:20px;margin-bottom:24px;align-items:start">
|
|
<div class="card" style="padding:16px;gap:10px">
|
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
<label style="font-size:12.5px;font-weight:500">Stopka wiadomości ({stopka})</label>
|
|
<button type="button" class="btn btn-ghost" style="padding:2px 8px;font-size:12px" wire:click="resetEmailFooter" wire:confirm="Przywrócić domyślną stopkę? Obecna treść zostanie zastąpiona.">Resetuj</button>
|
|
</div>
|
|
<x-quill-editor wire:key="email-footer-{{ $emailFooterVersion }}" :value="$emailFooterHtml" on-change="saveEmailFooter" min-height="70px" />
|
|
<span class="text-muted" style="font-size:11.5px">Wstawiana w pudełku w miejscu {stopka}; dopisywana do każdego wysyłanego powiadomienia.</span>
|
|
</div>
|
|
|
|
<div class="card" style="padding:0;overflow:hidden">
|
|
<div style="padding:10px 14px;border-bottom:1px solid var(--color-divider);font-size:12px;font-weight:500">Podgląd przykładowego e-maila</div>
|
|
<iframe title="Podgląd e-maila" srcdoc="{{ $this->emailPreviewDocument }}" style="width:100%;height:460px;border:0;background:#f2f2f5"></iframe>
|
|
</div>
|
|
</div>
|
|
|
|
<livewire:admin.mail-settings />
|
|
@endif
|
|
|
|
{{-- ================= BRANDING ================= --}}
|
|
@if ($tab === 'branding')
|
|
<h3 style="margin:0 0 14px">Wygląd i branding</h3>
|
|
<div style="display:flex;flex-direction:column;gap:20px;max-width:760px">
|
|
<div class="card" style="padding:18px;gap:12px;max-width:480px">
|
|
<div class="field"><label>Nazwa firmy (widoczna w nagłówku)</label><input class="input" value="{{ \App\Support\Settings::get('company_name') }}" wire:change="setCompanyName($event.target.value)"></div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Motyw jasny / ciemny / auto ustawia się przełącznikiem w nagłówku — dotyczy całej aplikacji.</p>
|
|
</div>
|
|
|
|
<div class="card" style="padding:18px;gap:12px;max-width:480px">
|
|
<div class="field">
|
|
<label>Kolor akcentu (przyciski, linki, znaczniki)</label>
|
|
<div style="display:flex;align-items:center;gap:10px">
|
|
<input type="color" style="width:44px;height:36px;border:1px solid var(--color-divider);border-radius:6px;background:transparent;cursor:pointer;padding:2px" value="{{ $accentColor }}" wire:change="setAccentColor($event.target.value)">
|
|
<input class="input" style="flex:1" value="{{ $accentColor }}" wire:change="setAccentColor($event.target.value)">
|
|
<button type="button" class="btn btn-ghost" wire:click="resetAccentColor">Domyślny</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Zastępuje domyślny fioletowy kolor przycisków, linków i etykiet w całej aplikacji.</p>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:20px;flex-wrap:wrap">
|
|
<div class="card" style="padding:18px;gap:8px;flex:1;min-width:260px">
|
|
<div class="field"><label>Logo</label></div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Widoczne na ekranie logowania. Bez własnego pliku używany jest domyślny znak.</p>
|
|
<img src="{{ \App\Support\Settings::logoUrl() }}" style="max-width:160px;max-height:90px;border-radius:12px;border:1px solid var(--color-divider);padding:8px;align-self:flex-start">
|
|
<div style="display:flex;gap:8px">
|
|
<label class="btn btn-secondary" style="cursor:pointer">Wgraj logo<input type="file" style="display:none" wire:model="logoUpload"></label>
|
|
@if (\App\Support\Settings::get('company_logo_path'))
|
|
<button type="button" class="btn btn-ghost" wire:click="removeLogo">Usuń logo</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:18px;gap:8px;flex:1;min-width:260px">
|
|
<div class="field"><label>Favicon (SVG)</label></div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Ikona karty przeglądarki. Bez własnego pliku używany jest domyślny znak.</p>
|
|
<img src="{{ \App\Support\Settings::faviconUrl() }}" style="width:40px;height:40px;border-radius:8px;border:1px solid var(--color-divider);padding:6px;align-self:flex-start">
|
|
@if ($faviconError)
|
|
<span style="color:var(--color-danger);font-size:12px">{{ $faviconError }}</span>
|
|
@endif
|
|
<div style="display:flex;gap:8px">
|
|
<label class="btn btn-secondary" style="cursor:pointer">Wgraj favicon<input type="file" accept=".svg,image/svg+xml" style="display:none" wire:model="faviconUpload"></label>
|
|
@if (\App\Support\Settings::get('favicon_path'))
|
|
<button type="button" class="btn btn-ghost" wire:click="removeFavicon">Usuń favicon</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:18px;gap:12px;max-width:620px">
|
|
<div class="field"><label>Komunikat na stronie logowania</label></div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Wyświetlany nad formularzem logowania, nad polami „Nazwa użytkownika” / „Hasło”. Obsługuje wiele akapitów, pogrubienia, listy itd.</p>
|
|
|
|
<div class="seg" style="align-self:flex-start">
|
|
@foreach (\App\Support\Settings::loginNoticeTypes() as $key => $label)
|
|
<label class="seg-opt">
|
|
<input type="radio" name="loginNoticeType" @checked($loginNoticeType === $key) wire:click="setLoginNoticeType('{{ $key }}')">
|
|
{{ $label }}
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
<x-quill-editor wire:key="login-notice-{{ $loginNoticeVersion }}" :value="$loginNoticeHtml" on-change="saveLoginNotice" min-height="90px" />
|
|
|
|
<div>
|
|
<div class="card-kicker" style="margin-bottom:6px">Podgląd</div>
|
|
<x-login-notice :html="$loginNoticeHtml" :type="$loginNoticeType" />
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-ghost" style="align-self:flex-start;padding:0" wire:click="resetLoginNotice" wire:confirm="Przywrócić domyślny komunikat? Obecna treść zostanie zastąpiona.">Przywróć domyślny</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= CONFIG ================= --}}
|
|
@if ($tab === 'config')
|
|
<h3 style="margin:0 0 16px">Konfiguracja</h3>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:20px;align-items:start">
|
|
|
|
<form wire:submit="saveSystemConfig" style="display:contents">
|
|
<div class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Zgłoszenia</h4>
|
|
<div class="field">
|
|
<label>Domyślny status nowego zgłoszenia</label>
|
|
<select class="input" wire:model="systemConfig.defaultStatus">
|
|
@foreach ($this->statuses as $s)
|
|
<option value="{{ $s->key }}">{{ $s->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<label class="radio"><input type="checkbox" wire:model="systemConfig.autoAssignByCategory" style="position:static;opacity:1;width:auto;height:auto">Automatyczne przypisywanie do zespołu wg kategorii</label>
|
|
|
|
<div class="hr"></div>
|
|
|
|
<div class="field">
|
|
<label>Prefiks numeru zgłoszenia</label>
|
|
<input class="input" maxlength="20" placeholder="#" wire:model.live="systemConfig.ticketNumberPrefix">
|
|
</div>
|
|
<div class="field">
|
|
<label>Minimalna długość numeru (uzupełniana zerami z przodu)</label>
|
|
<input class="input" type="number" min="1" max="10" wire:model.live="systemConfig.ticketNumberMinLength">
|
|
</div>
|
|
<label class="radio"><input type="checkbox" wire:model.live="systemConfig.ticketNumberObfuscate" style="position:static;opacity:1;width:auto;height:auto">Ukryj kolejność zgłoszeń (numer wyświetlany jako suma kontrolna zamiast kolejnego numeru)</label>
|
|
<div class="text-muted" style="font-size:12px">
|
|
ID z bazy: {{ $this->ticketNumberPreview['id'] }} → podgląd numeru: {{ $this->ticketNumberPreview['formatted'] }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Załączniki</h4>
|
|
<label class="radio"><input type="checkbox" wire:model="systemConfig.allowAttachments" style="position:static;opacity:1;width:auto;height:auto">Zezwalaj na załączniki w zgłoszeniach</label>
|
|
@if ($systemConfig['allowAttachments'])
|
|
<div style="display:flex;gap:10px">
|
|
<div class="field" style="flex:1"><label>Maks. rozmiar załącznika (KB)</label><input class="input" type="number" wire:model="systemConfig.attachmentMaxSizeKb"></div>
|
|
<div class="field" style="flex:1"><label>Maks. liczba załączników</label><input class="input" type="number" wire:model="systemConfig.attachmentMaxCount"></div>
|
|
</div>
|
|
<div class="field"><label>Maks. łączny rozmiar załączników (KB)</label><input class="input" type="number" wire:model="systemConfig.attachmentMaxTotalSizeKb"></div>
|
|
<div class="field"><label>Dozwolone rozszerzenia</label><input class="input" placeholder="jpg,png,pdf,..." wire:model="systemConfig.attachmentAllowedTypes"></div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Sesja i strefa czasowa</h4>
|
|
<div class="field"><label>Czas wygaśnięcia sesji (minuty)</label><input class="input" type="number" wire:model="systemConfig.sessionLifetimeMinutes"></div>
|
|
<div class="field"
|
|
x-data="{
|
|
tz: @js($systemConfig['timezone']),
|
|
now: '',
|
|
tick() {
|
|
try {
|
|
this.now = new Intl.DateTimeFormat('pl-PL', { timeZone: this.tz, dateStyle: 'medium', timeStyle: 'medium' }).format(new Date());
|
|
} catch (e) {
|
|
this.now = '—';
|
|
}
|
|
},
|
|
}"
|
|
x-init="tick(); setInterval(() => tick(), 1000)">
|
|
<label>Strefa czasowa</label>
|
|
<select class="input" wire:model="systemConfig.timezone" x-on:change="tz = $event.target.value; tick()">
|
|
@foreach (\DateTimeZone::listIdentifiers() as $tzId)
|
|
<option value="{{ $tzId }}">{{ $tzId }}</option>
|
|
@endforeach
|
|
</select>
|
|
<div style="display:flex;gap:4px;font-size:12px">
|
|
<span class="text-muted">Aktualny czas:</span>
|
|
<strong x-text="now"></strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Częstotliwość odświeżania i harmonogramu</h4>
|
|
|
|
<div class="card-kicker">Odświeżanie w przeglądarce</div>
|
|
<div style="display:flex;gap:10px">
|
|
<div class="field" style="flex:1"><label>Widok zgłoszenia (klient i operator)</label><input class="input" type="number" min="1" wire:model="systemConfig.refreshTicketViewSeconds"></div>
|
|
<div class="field" style="flex:1"><label>Lista zgłoszeń operatora</label><input class="input" type="number" min="1" wire:model="systemConfig.refreshQueueSeconds"></div>
|
|
</div>
|
|
<div class="field"><label>Dzwonek powiadomień</label><input class="input" type="number" min="1" wire:model="systemConfig.refreshNotificationsSeconds"></div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Sekundy między automatycznymi odświeżeniami w przeglądarce (niezależnie od odświeżenia na żądanie po kliknięciu w licznik).</p>
|
|
|
|
<div class="hr"></div>
|
|
|
|
<div class="card-kicker">Zadania w tle</div>
|
|
<div style="display:flex;gap:10px">
|
|
<div class="field" style="flex:1"><label>Sprawdzanie naruszeń SLA</label><input class="input" type="number" min="1" wire:model="systemConfig.scheduleSlaCheckMinutes"></div>
|
|
<div class="field" style="flex:1"><label>Reguły automatyzacji</label><input class="input" type="number" min="1" wire:model="systemConfig.scheduleAutomationRulesMinutes"></div>
|
|
</div>
|
|
<div style="display:flex;gap:10px">
|
|
<div class="field" style="flex:1"><label>Pobieranie e-maili (IMAP)</label><input class="input" type="number" min="1" wire:model="systemConfig.scheduleImapFetchMinutes"></div>
|
|
<div class="field" style="flex:1"><label>Automatyczna kategoryzacja i podsumowania AI</label><input class="input" type="number" min="1" wire:model="systemConfig.scheduleAiAutomationMinutes"></div>
|
|
</div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Minuty między uruchomieniami zadań w tle. Zmiana obowiązuje od najbliższego uruchomienia harmonogramu (co minutę), bez potrzeby restartu.</p>
|
|
</div>
|
|
|
|
<div style="grid-column:1/-1;display:flex">
|
|
<button type="submit" class="btn btn-primary">Zapisz</button>
|
|
</div>
|
|
</form>
|
|
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= INTEGRATIONS ================= --}}
|
|
@if ($tab === 'integrations')
|
|
<h3 style="margin:0 0 16px">Integracje</h3>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:20px;align-items:start">
|
|
|
|
<form wire:submit="saveLdapConfig" class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">LDAP / Active Directory</h4>
|
|
<label class="radio"><input type="checkbox" wire:model="ldapConfig.enabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz autentykację LDAP/AD</strong></label>
|
|
|
|
@if ($ldapConfig['enabled'])
|
|
<div class="field"><label>Host serwera LDAP</label><input class="input" placeholder="ldap.example.com" wire:model="ldapConfig.host"></div>
|
|
<div style="display:flex;gap:10px">
|
|
<div class="field" style="flex:1"><label>Port</label><input class="input" type="number" placeholder="389" wire:model="ldapConfig.port"></div>
|
|
<div style="flex:none;padding-top:26px"><label class="radio"><input type="checkbox" wire:model="ldapConfig.useSsl" style="position:static;opacity:1;width:auto;height:auto">LDAPS (SSL)</label></div>
|
|
</div>
|
|
<div class="field"><label>Base DN</label><input class="input" placeholder="dc=example,dc=com" wire:model="ldapConfig.baseDn"></div>
|
|
<div class="field"><label>Bind DN</label><input class="input" placeholder="cn=admin,dc=example,dc=com" wire:model="ldapConfig.bindDn"></div>
|
|
<div class="field"><label>Hasło Bind</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="ldapConfig.bindPassword"></div>
|
|
<div class="field"><label>User Filter</label><input class="input" placeholder="(uid={0})" wire:model="ldapConfig.userFilter"></div>
|
|
<label class="radio"><input type="checkbox" wire:model="ldapConfig.autoProvisionGuests" style="position:static;opacity:1;width:auto;height:auto">Automatycznie zakładaj konto dla gościa zgłaszającego, jeśli jego e-mail istnieje w LDAP</label>
|
|
<label class="radio"><input type="checkbox" wire:model="ldapConfig.restrictUserCreationToLdap" style="position:static;opacity:1;width:auto;height:auto">Zezwalaj na ręczne zapraszanie użytkowników tylko, jeśli ich e-mail istnieje w LDAP</label>
|
|
<label class="radio"><input type="checkbox" wire:model="ldapConfig.restrictTicketsToLdap" style="position:static;opacity:1;width:auto;height:auto">Zezwalaj na tworzenie zgłoszeń bez logowania tylko dla adresów e-mail istniejących w LDAP</label>
|
|
|
|
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
|
|
<button type="button" class="btn btn-secondary" wire:click="testLdapConnection">Testuj połączenie</button>
|
|
<button type="submit" class="btn btn-primary">Zapisz</button>
|
|
@if ($ldapTestResult === 'ok')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Połączenie OK</div>
|
|
@elseif ($ldapTestResult === 'error')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd połączenia</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
|
|
@endif
|
|
</form>
|
|
|
|
<form wire:submit="saveBookstackConfig" class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Baza wiedzy BookStack</h4>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Po włączeniu, podczas tworzenia zgłoszenia klientom i operatorom podpowiadane będą pasujące artykuły z BookStack na podstawie wybranej kategorii/podkategorii.</span>
|
|
<label class="radio"><input type="checkbox" wire:model="bookstackConfig.enabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz integrację z BookStack</strong></label>
|
|
|
|
@if ($bookstackConfig['enabled'])
|
|
<div class="field"><label>Adres instancji BookStack</label><input class="input" placeholder="https://wiki.firma.pl" wire:model="bookstackConfig.baseUrl"></div>
|
|
<div class="field"><label>Token ID</label><input class="input" wire:model="bookstackConfig.tokenId"></div>
|
|
<div class="field"><label>Token Secret</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="bookstackConfig.tokenSecret"></div>
|
|
<p class="text-muted" style="font-size:11.5px;margin:-4px 0 0">Token API generuje się w BookStack: Profil → Ustawienia API. Użytkownik/rola właściciela tokenu musi mieć uprawnienie „Access System API”.</p>
|
|
|
|
<div class="field">
|
|
<label>Przeszukuj</label>
|
|
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
|
<label style="display:flex;align-items:center;gap:6px;font-size:13px;font-weight:400">
|
|
<input type="checkbox" style="position:static;opacity:1;width:auto;height:auto" @checked(in_array('book', $bookstackConfig['searchTypes'])) wire:click="toggleBookstackSearchType('book')">
|
|
Książki
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:6px;font-size:13px;font-weight:400">
|
|
<input type="checkbox" style="position:static;opacity:1;width:auto;height:auto" @checked(in_array('page', $bookstackConfig['searchTypes'])) wire:click="toggleBookstackSearchType('page')">
|
|
Strony
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:6px;font-size:13px;font-weight:400">
|
|
<input type="checkbox" style="position:static;opacity:1;width:auto;height:auto" @checked(in_array('chapter', $bookstackConfig['searchTypes'])) wire:click="toggleBookstackSearchType('chapter')">
|
|
Rozdziały
|
|
</label>
|
|
</div>
|
|
@if (empty($bookstackConfig['searchTypes']))
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Nic nie zaznaczono — przy zapisie zostaną użyte wszystkie typy.</p>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Szukaj po</label>
|
|
<select class="input" style="width:auto" wire:model="bookstackConfig.searchBy">
|
|
<option value="both">Słowa kluczowe w nazwie lub tagi</option>
|
|
<option value="name">Tylko słowa kluczowe w nazwie</option>
|
|
<option value="tags">Tylko tagi</option>
|
|
</select>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">„Tagi” dopasowują artykuły oznaczone w BookStack tagiem o nazwie zgodnej z kategorią/podkategorią zgłoszenia (np. tag „Drukarki” dla podkategorii „Drukarki”).</p>
|
|
</div>
|
|
|
|
<div style="display:flex;justify-content:flex-end">
|
|
<button type="button" class="btn btn-secondary" style="display:flex;align-items:center;gap:6px;font-size:12.5px" wire:click="refreshBookstackShelves" wire:loading.attr="disabled" wire:target="refreshBookstackShelves">
|
|
<span class="material-symbols-outlined" style="font-size:16px" wire:loading.class="spin" wire:target="refreshBookstackShelves">refresh</span>
|
|
Odśwież listę półek
|
|
</button>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Dozwolone półki — podpowiedzi przy tworzeniu zgłoszenia</label>
|
|
@if (count($this->bookstackShelves))
|
|
<div style="display:flex;flex-direction:column;gap:2px;border:1px solid var(--color-divider);border-radius:8px;padding:8px">
|
|
@foreach ($this->bookstackShelves as $shelf)
|
|
<label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:400;padding:4px 6px;border-radius:5px;cursor:pointer">
|
|
<input type="checkbox" @checked(in_array($shelf['id'], $bookstackConfig['allowedShelfIdsCreation'])) wire:click="toggleBookstackAllowedShelf('allowedShelfIdsCreation', {{ $shelf['id'] }})">
|
|
{{ $shelf['name'] }}
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Brak półek do wyświetlenia — sprawdź, czy połączenie działa (przycisk „Testuj połączenie” niżej), albo zapisz konfigurację, żeby odświeżyć listę.</p>
|
|
@endif
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Tylko książki/strony z zaznaczonych półek mogą pojawić się jako podpowiedzi podczas tworzenia zgłoszenia (klient, operator, formularz gościa). Jeśli żadna półka nie jest zaznaczona, podpowiedzi się nie pojawią.</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Dozwolone półki — panel operatora przy zgłoszeniu</label>
|
|
@if (count($this->bookstackShelves))
|
|
<div style="display:flex;flex-direction:column;gap:2px;border:1px solid var(--color-divider);border-radius:8px;padding:8px">
|
|
@foreach ($this->bookstackShelves as $shelf)
|
|
<label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:400;padding:4px 6px;border-radius:5px;cursor:pointer">
|
|
<input type="checkbox" @checked(in_array($shelf['id'], $bookstackConfig['allowedShelfIdsTicketView'])) wire:click="toggleBookstackAllowedShelf('allowedShelfIdsTicketView', {{ $shelf['id'] }})">
|
|
{{ $shelf['name'] }}
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Brak półek do wyświetlenia — sprawdź, czy połączenie działa (przycisk „Testuj połączenie” niżej), albo zapisz konfigurację, żeby odświeżyć listę.</p>
|
|
@endif
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Niezależna lista — kontroluje, co widzi operator w bocznym panelu po otwarciu istniejącego zgłoszenia (link do artykułu lub przycisk kopiowania linku). Jeśli żadna półka nie jest zaznaczona, panel się nie pokaże.</p>
|
|
</div>
|
|
|
|
<label class="radio"><input type="checkbox" wire:model="bookstackConfig.showToGuests" style="position:static;opacity:1;width:auto;height:auto">Pokazuj podpowiedzi także niezalogowanym (formularz zgłoszenia na stronie głównej)</label>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Bez zaznaczenia podpowiedzi widoczne są tylko przy tworzeniu zgłoszenia przez zalogowanego klienta lub operatora.</span>
|
|
|
|
<label class="radio"><input type="checkbox" wire:model="bookstackConfig.verifySsl" style="position:static;opacity:1;width:auto;height:auto">Weryfikuj certyfikat SSL instancji BookStack</label>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Wyłącz tylko jeśli instancja BookStack korzysta z certyfikatu self-signed / z prywatnego CA.</span>
|
|
|
|
<div class="field">
|
|
<label>Automatyczne tagowanie treści (AI)</label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Używa integracji AI (skonfigurowanej w karcie „Integracja AI” obok) do otagowania książek/stron/rozdziałów nazwami pasujących podkategorii helpdesku — bez tego wyszukiwanie „po tagach” nic nie znajdzie. „Otaguj nową treść” pomija już otagowane pozycje; „Otaguj wszystko ponownie” klasyfikuje od nowa całą wiki (dłużej, więcej zapytań do AI).</p>
|
|
<div style="display:flex;gap:10px;margin-top:6px;align-items:center;flex-wrap:wrap">
|
|
<button type="button" class="btn btn-secondary" wire:click="runBookstackTagging" wire:loading.attr="disabled" wire:target="runBookstackTagging,runBookstackTaggingForce">
|
|
<span class="material-symbols-outlined" style="font-size:16px;vertical-align:middle" wire:loading.class="spin" wire:target="runBookstackTagging">sell</span>
|
|
Otaguj nową treść
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" wire:click="runBookstackTaggingForce" wire:loading.attr="disabled" wire:target="runBookstackTagging,runBookstackTaggingForce">
|
|
<span class="material-symbols-outlined" style="font-size:16px;vertical-align:middle" wire:loading.class="spin" wire:target="runBookstackTaggingForce">refresh</span>
|
|
Otaguj wszystko ponownie (force)
|
|
</button>
|
|
</div>
|
|
@if ($bookstackTagResult !== null)
|
|
<div class="text-muted" style="font-size:11.5px;margin-top:6px">Przeskanowano {{ $bookstackTagResult['scanned'] }}, otagowano {{ $bookstackTagResult['tagged'] }}, pominięto {{ $bookstackTagResult['skipped'] }}, nieudanych paczek {{ $bookstackTagResult['failed_batches'] }}.</div>
|
|
@elseif ($bookstackTagError)
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger);font-size:11.5px;margin-top:6px"><span class="material-symbols-outlined" style="font-size:16px">error</span>{{ $bookstackTagError }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
|
|
<button type="button" class="btn btn-secondary" wire:click="testBookstackConnection">Testuj połączenie</button>
|
|
<button type="submit" class="btn btn-primary">Zapisz</button>
|
|
@if ($bookstackTestResult === 'ok')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Połączenie OK</div>
|
|
@elseif ($bookstackTestResult === 'error')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd połączenia{{ $bookstackTestMessage ? ': '.$bookstackTestMessage : '' }}</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
|
|
@endif
|
|
</form>
|
|
|
|
<form wire:submit="saveSnipeitConfig" class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Snipe-IT (ewidencja sprzętu)</h4>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Pokazuje sprzęt przypisany do zgłaszającego przy tworzeniu i przeglądaniu zgłoszenia oraz pozwala powiązać zgłoszenie z konkretnym urządzeniem z ewidencji Snipe-IT.</span>
|
|
<label class="radio"><input type="checkbox" wire:model="snipeitConfig.enabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz integrację z Snipe-IT</strong></label>
|
|
|
|
@if ($snipeitConfig['enabled'])
|
|
<div class="field"><label>Adres API</label><input class="input" placeholder="https://assets.firma.pl" wire:model="snipeitConfig.baseUrl"></div>
|
|
<div class="field"><label>Klucz API</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="snipeitConfig.apiToken"></div>
|
|
<p class="text-muted" style="font-size:11.5px;margin:-4px 0 0">Osobisty token API generuje się w Snipe-IT: profil użytkownika → „Create New Token”.</p>
|
|
|
|
<label class="radio"><input type="checkbox" wire:model="snipeitConfig.skipSslVerification" style="position:static;opacity:1;width:auto;height:auto">Nie sprawdzaj SSL</label>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Zaznacz tylko, jeśli instancja Snipe-IT korzysta z certyfikatu self-signed / z prywatnego CA.</span>
|
|
|
|
<div style="border-top:1px solid var(--color-divider);margin:4px 0"></div>
|
|
|
|
<div class="field">
|
|
<label>Klient</label>
|
|
<label class="radio"><input type="checkbox" wire:model="snipeitConfig.clientCanSelectAsset" style="position:static;opacity:1;width:auto;height:auto">Klient może wybrać sprzęt, którego dotyczy zgłoszenie</label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Przy tworzeniu zgłoszenia klient zobaczy listę swojego sprzętu z Snipe-IT (dopasowanego po adresie e-mail) i będzie mógł je powiązać ze zgłoszeniem.</p>
|
|
</div>
|
|
|
|
@if ($snipeitConfig['clientCanSelectAsset'])
|
|
<div class="field">
|
|
<label>Ogranicz do podkategorii</label>
|
|
<x-multiselect
|
|
:options="$this->subcategoriesForTeamForm"
|
|
:selected-ids="$snipeitConfig['clientAssetSubcategoryIds']"
|
|
toggle-action="toggleSnipeitClientSubcategory"
|
|
placeholder="Brak wybranych podkategorii"
|
|
/>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Wybór sprzętu pojawi się klientowi tylko przy tworzeniu zgłoszenia w zaznaczonych tu podkategoriach. Jeśli nic nie jest zaznaczone, opcja nie pojawi się w żadnej podkategorii.</p>
|
|
</div>
|
|
@endif
|
|
|
|
<div style="border-top:1px solid var(--color-divider);margin:4px 0"></div>
|
|
|
|
<div class="field">
|
|
<label>Operator</label>
|
|
<label class="radio"><input type="checkbox" wire:model="snipeitConfig.operatorViewRequesterAssets" style="position:static;opacity:1;width:auto;height:auto">Operator może zobaczyć sprzęt zgłaszającego w widoku zgłoszenia</label>
|
|
<label class="radio"><input type="checkbox" wire:model="snipeitConfig.operatorSearchInventory" style="position:static;opacity:1;width:auto;height:auto">Zezwól operatorowi na przeszukiwanie całego inwentarza (nie tylko sprzętu zgłaszającego)</label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Obie funkcje pojawiają się w bocznym panelu widoku zgłoszenia operatora — przeszukiwanie inwentarza jako pole wyszukiwania z przyciskiem „Szukaj”, nie osobna podstrona. Odpięcie już powiązanego urządzenia jest zawsze dostępne dla operatora, niezależnie od tych dwóch ustawień.</p>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
|
|
<button type="button" class="btn btn-secondary" wire:click="testSnipeitConnection">Testuj połączenie</button>
|
|
<button type="submit" class="btn btn-primary">Zapisz</button>
|
|
@if ($snipeitTestResult === 'ok')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Połączenie OK</div>
|
|
@elseif ($snipeitTestResult === 'error')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd połączenia{{ $snipeitTestMessage ? ': '.$snipeitTestMessage : '' }}</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
|
|
@endif
|
|
</form>
|
|
|
|
<form wire:submit="saveAiConfig" class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Integracja AI</h4>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Ogólne połączenie z dostawcą modelu językowego (API kompatybilne z OpenAI — Groq, OpenAI, lokalny Ollama itp.), wykorzystywane m.in. do automatycznego tagowania treści w BookStack.</span>
|
|
<label class="radio"><input type="checkbox" wire:model="aiConfig.enabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Włącz integrację AI</strong></label>
|
|
|
|
@if ($aiConfig['enabled'])
|
|
<div class="field"><label>Adres API (Base URL)</label><input class="input" placeholder="https://api.groq.com/openai/v1" wire:model="aiConfig.baseUrl"></div>
|
|
<div class="field"><label>Klucz API</label><input class="input" type="password" placeholder="(bez zmian jeśli puste)" wire:model="aiConfig.apiKey"></div>
|
|
<p class="text-muted" style="font-size:11.5px;margin:-4px 0 0">Zostaw puste dla lokalnych instancji bez autoryzacji (np. Ollama).</p>
|
|
<div class="field"><label>Model</label><input class="input" placeholder="np. llama-3.3-70b-versatile" wire:model="aiConfig.model"></div>
|
|
|
|
<label class="radio"><input type="checkbox" wire:model="aiConfig.verifySsl" style="position:static;opacity:1;width:auto;height:auto">Weryfikuj certyfikat SSL</label>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Wyłącz tylko jeśli instancja (np. lokalny Ollama) korzysta z certyfikatu self-signed / z prywatnego CA.</span>
|
|
|
|
<div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap">
|
|
<button type="button" class="btn btn-secondary" wire:click="testAiConnection">Testuj połączenie</button>
|
|
<button type="submit" class="btn btn-primary">Zapisz</button>
|
|
@if ($aiTestResult === 'ok')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-success)"><span class="material-symbols-outlined" style="font-size:18px">check_circle</span>Połączenie OK</div>
|
|
@elseif ($aiTestResult === 'error')
|
|
<div style="display:flex;align-items:center;gap:6px;color:var(--color-danger)"><span class="material-symbols-outlined" style="font-size:18px">error</span>Błąd połączenia{{ $aiTestMessage ? ': '.$aiTestMessage : '' }}</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
|
|
@endif
|
|
</form>
|
|
|
|
<form wire:submit="saveAiTriageConfig" class="card" style="padding:20px;gap:14px">
|
|
<h4 style="margin:0">Automatyzacja AI dla zgłoszeń</h4>
|
|
<span class="text-muted" style="font-size:11.5px;margin-top:-8px">Wymaga włączonej integracji AI (karta obok). Zgłoszenia są przetwarzane w tle, cyklicznie co kilka minut — nie spowalnia to tworzenia zgłoszenia przez klienta.</span>
|
|
|
|
<div class="field">
|
|
<label>Automatyczna kategoryzacja nowych zgłoszeń</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiTriageConfig.categoryWhenMissing" style="position:static;opacity:1;width:auto;height:auto">Przypisz kategorię/podkategorię, gdy zgłoszenie nie ma żadnej</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiTriageConfig.subcategoryWhenCategoryOnly" style="position:static;opacity:1;width:auto;height:auto">Dobierz podkategorię, gdy zgłoszenie ma tylko kategorię</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiTriageConfig.recheckCategorized" style="position:static;opacity:1;width:auto;height:auto">Zweryfikuj i ewentualnie popraw już przypisaną podkategorię</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiTriageConfig.fixSubject" style="position:static;opacity:1;width:auto;height:auto">Popraw temat zgłoszenia, jeśli jest niejasny</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiTriageConfig.setPriority" style="position:static;opacity:1;width:auto;height:auto">Ustaw priorytet na podstawie treści</label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Każde zgłoszenie jest sprawdzane tylko raz — zastosowane zmiany trafiają do historii zgłoszenia z adnotacją „Automatyzacja: klasyfikacja AI”.</p>
|
|
</div>
|
|
|
|
<div style="border-top:1px solid var(--color-divider);margin:4px 0"></div>
|
|
|
|
<div class="field">
|
|
<label>Podsumowanie AI dla operatora</label>
|
|
<label class="radio"><input type="checkbox" wire:model="aiSummaryEnabled" style="position:static;opacity:1;width:auto;height:auto"><strong>Generuj podsumowanie i sugerowaną akcję dla każdego zgłoszenia</strong></label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Widoczne wyłącznie w panelu operatora, w bocznym panelu zgłoszenia. Domyślnie odświeżane cyklicznie (co kilka minut, wraz z pozostałą automatyzacją AI powyżej).</p>
|
|
|
|
<label class="radio"><input type="checkbox" wire:model="aiSummaryRegenerateOnMessage" style="position:static;opacity:1;width:auto;height:auto">Regeneruj podsumowanie od razu po każdej nowej wiadomości</label>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Zamiast czekać na najbliższy cykl automatyzacji — dotyczy odpowiedzi operatora, klienta i notatek wewnętrznych.</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
<label>Prompt systemowy podsumowania</label>
|
|
<button type="button" class="btn btn-ghost" style="padding:2px 8px;font-size:12px" wire:click="resetAiSummaryPrompt" wire:confirm="Przywrócić domyślny prompt? Obecna treść zostanie zastąpiona.">Resetuj</button>
|
|
</div>
|
|
<textarea wire:key="ai-summary-prompt-{{ $aiSummaryPromptVersion }}" class="input" rows="6" wire:change="saveAiSummaryPrompt($event.target.value)">{{ $aiSummaryPrompt }}</textarea>
|
|
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Model musi zwrócić obiekt JSON z kluczami "summary" i "suggested_action" — nie zmieniaj tego wymogu, chyba że wiadomo, że nowy dostawca/model obsłuży to inaczej.</p>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary" style="align-self:flex-start">Zapisz</button>
|
|
</form>
|
|
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ================= ABOUT ================= --}}
|
|
{{-- ================= API KEYS ================= --}}
|
|
@if ($tab === 'api-keys')
|
|
<livewire:admin.api-keys />
|
|
@endif
|
|
|
|
@if ($tab === 'about')
|
|
<h3 style="margin:0 0 14px">O aplikacji</h3>
|
|
<div class="card" style="padding:18px;gap:10px;max-width:420px">
|
|
<div style="display:flex;justify-content:space-between"><span class="text-muted">Aplikacja</span><span>{{ \App\Support\Settings::get('company_name') }}</span></div>
|
|
<div style="display:flex;justify-content:space-between"><span class="text-muted">Wersja</span><span>{{ config('app.version') ?: '—' }}</span></div>
|
|
<div style="display:flex;justify-content:space-between"><span class="text-muted">Kontakt wsparcia</span><span>{{ config('app.author_contact') ?: '—' }}</span></div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ================= DIALOGS ================= --}}
|
|
|
|
@if ($replyQuickActionFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitReplyQuickAction" class="dialog" style="max-width:440px">
|
|
<div class="dialog-title">{{ $replyQuickActionForm['id'] ? 'Edytuj szybką akcję' : 'Nowa szybka akcja' }}</div>
|
|
<div class="field"><label>Nazwa przycisku</label><input class="input" wire:model="replyQuickActionForm.label" placeholder="np. Wyślij i zamknij"></div>
|
|
@error('replyQuickActionForm.label') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
<div class="field">
|
|
<label>Po wysłaniu zmień status na</label>
|
|
<select class="input" wire:model="replyQuickActionForm.statusKey">
|
|
<option value="">Nie zmieniaj</option>
|
|
@foreach ($this->statuses as $s)
|
|
<option value="{{ $s->key }}">{{ $s->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Kolejność</label>
|
|
<input class="input" type="number" placeholder="(automatycznie na końcu)" wire:model="replyQuickActionForm.sortOrder">
|
|
@error('replyQuickActionForm.sortOrder') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
</div>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeReplyQuickActionForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $replyQuickActionForm['id'] ? 'Zapisz' : 'Dodaj akcję' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($responseTemplateFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitResponseTemplate" class="dialog" style="max-width:520px">
|
|
<div class="dialog-title">{{ $responseTemplateForm['id'] ? 'Edytuj szablon odpowiedzi' : 'Nowy szablon odpowiedzi' }}</div>
|
|
<div class="field"><label>Nazwa szablonu</label><input class="input" wire:model="responseTemplateForm.label" placeholder="np. Prośba o więcej informacji"></div>
|
|
@error('responseTemplateForm.label') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
<div class="field">
|
|
<label>Treść</label>
|
|
<textarea class="input" style="min-height:140px" wire:model="responseTemplateForm.body"></textarea>
|
|
</div>
|
|
@error('responseTemplateForm.body') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeResponseTemplateForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $responseTemplateForm['id'] ? 'Zapisz' : 'Dodaj szablon' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($automationRuleFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitAutomationRule" class="dialog" style="max-width:520px">
|
|
<div class="dialog-title">{{ $automationRuleForm['id'] ? 'Edytuj regułę automatyzacji' : 'Nowa reguła automatyzacji' }}</div>
|
|
<div class="field"><label>Nazwa reguły</label><input class="input" wire:model="automationRuleForm.label" placeholder="np. Eskalacja przy braku odpowiedzi"></div>
|
|
@error('automationRuleForm.label') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
<div class="field">
|
|
<label>Brak odpowiedzi klienta przez (minuty)</label>
|
|
<input class="input" type="number" min="1" wire:model="automationRuleForm.condition_minutes">
|
|
</div>
|
|
@error('automationRuleForm.condition_minutes') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
|
|
<div class="field">
|
|
<label>Zawężenie (opcjonalne — puste = dowolne)</label>
|
|
<select class="input" wire:model="automationRuleForm.scope_priority_key" style="margin-bottom:6px">
|
|
<option value="">Dowolny priorytet</option>
|
|
@foreach ($this->priorities as $p)
|
|
<option value="{{ $p->key }}">{{ $p->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
<select class="input" wire:model="automationRuleForm.scope_subcategory_id" style="margin-bottom:6px">
|
|
<option value="">Dowolna kategoria</option>
|
|
@foreach ($this->subcategoriesForTeamForm as $s)
|
|
<option value="{{ $s['id'] }}">{{ $s['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
<select class="input" wire:model="automationRuleForm.scope_team_id">
|
|
<option value="">Dowolny zespół</option>
|
|
@foreach ($this->teams as $t)
|
|
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Akcja</label>
|
|
<select class="input" wire:model.live="automationRuleForm.action_type">
|
|
<option value="change_priority">Zmień priorytet</option>
|
|
<option value="change_status">Zmień status</option>
|
|
<option value="change_team">Zmień zespół</option>
|
|
<option value="change_assignee">Zmień przypisanie</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Nowa wartość</label>
|
|
@switch ($automationRuleForm['action_type'])
|
|
@case ('change_priority')
|
|
<select class="input" wire:model="automationRuleForm.action_value">
|
|
<option value="">— wybierz —</option>
|
|
@foreach ($this->priorities as $p)
|
|
<option value="{{ $p->key }}">{{ $p->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
@break
|
|
@case ('change_status')
|
|
<select class="input" wire:model="automationRuleForm.action_value">
|
|
<option value="">— wybierz —</option>
|
|
@foreach ($this->statuses as $s)
|
|
<option value="{{ $s->key }}">{{ $s->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
@break
|
|
@case ('change_team')
|
|
<select class="input" wire:model="automationRuleForm.action_value">
|
|
<option value="">— wybierz —</option>
|
|
@foreach ($this->teams as $t)
|
|
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
@break
|
|
@case ('change_assignee')
|
|
<select class="input" wire:model="automationRuleForm.action_value">
|
|
<option value="">— wybierz —</option>
|
|
@foreach ($this->operatorsForTeamForm as $o)
|
|
<option value="{{ $o['id'] }}">{{ $o['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
@break
|
|
@endswitch
|
|
</div>
|
|
@error('automationRuleForm.action_value') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeAutomationRuleForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $automationRuleForm['id'] ? 'Zapisz' : 'Dodaj regułę' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($this->editingTemplate)
|
|
<div class="dialog-backdrop">
|
|
<div class="dialog" style="max-width:560px">
|
|
<div class="dialog-title">Edytuj szablon — {{ $this->editingTemplate->name }}</div>
|
|
<div class="field"><label>Temat wiadomości</label><input class="input" value="{{ $this->editingTemplate->subject }}" wire:change="setTemplateSubject($event.target.value)"></div>
|
|
<div class="field">
|
|
<label>Treść</label>
|
|
<x-quill-editor :value="$this->editingTemplate->body" on-change="setTemplateBody" min-height="160px" />
|
|
</div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Dostępne zmienne: {numer}, {imie}, {temat}, {status}, {kategoria}, {priorytet}, {zespol}, {operator}, {link} — {link} wstawia bezpośredni link do zgłoszenia. Ta treść trafia do wspólnego szablonu-pudełka (u góry listy) w miejscu {tresc}.</p>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-primary" type="button" wire:click="closeEmailTemplateEditor">Zamknij</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($fieldFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitField" class="dialog" style="max-width:480px">
|
|
<div class="dialog-title">{{ $fieldForm['id'] ? 'Edytuj pole' : 'Nowe pole dodatkowe' }}</div>
|
|
<div class="field"><label>Etykieta</label><input class="input" wire:model="fieldForm.label"></div>
|
|
<div class="field">
|
|
<label>Typ pola</label>
|
|
<select class="input" wire:model="fieldForm.type">
|
|
<option value="text">Tekst krótki</option>
|
|
<option value="textarea">Tekst długi</option>
|
|
<option value="select">Lista wyboru</option>
|
|
<option value="checkbox">Checkbox</option>
|
|
<option value="date">Data</option>
|
|
<option value="number">Liczba</option>
|
|
</select>
|
|
</div>
|
|
@if ($fieldForm['type'] === 'select')
|
|
<div class="field"><label>Opcje (oddzielone przecinkiem)</label><input class="input" wire:model="fieldForm.optionsText"></div>
|
|
@endif
|
|
<label class="radio"><input type="checkbox" wire:model="fieldForm.required" style="position:static;opacity:1;width:auto;height:auto">Pole wymagane</label>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeFieldForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $fieldForm['id'] ? 'Zapisz' : 'Dodaj pole' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($userFieldFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitUserField" class="dialog" style="max-width:480px">
|
|
<div class="dialog-title">{{ $userFieldForm['id'] ? 'Edytuj pole' : 'Nowe pole dodatkowe użytkownika' }}</div>
|
|
<div class="field"><label>Nazwa</label><input class="input" wire:model="userFieldForm.label"></div>
|
|
<div class="field"><label>Opis</label><textarea class="input" wire:model="userFieldForm.description" style="min-height:70px"></textarea></div>
|
|
<div class="field">
|
|
<label>Typ pola</label>
|
|
<select class="input" wire:model="userFieldForm.type">
|
|
<option value="text">Tekst krótki</option>
|
|
<option value="textarea">Tekst długi</option>
|
|
<option value="select">Lista wyboru</option>
|
|
<option value="checkbox">Checkbox</option>
|
|
<option value="date">Data</option>
|
|
<option value="number">Liczba</option>
|
|
</select>
|
|
</div>
|
|
@if ($userFieldForm['type'] === 'select')
|
|
<div class="field"><label>Opcje (oddzielone przecinkiem)</label><input class="input" wire:model="userFieldForm.optionsText"></div>
|
|
@endif
|
|
<div class="field">
|
|
<label>Atrybut LDAP (opcjonalnie)</label>
|
|
<input class="input" placeholder="np. mobile, department, employeeNumber" wire:model="userFieldForm.ldapAttribute">
|
|
</div>
|
|
<p class="text-muted" style="font-size:12px;margin:0">Jeśli podane, wartość tego pola będzie automatycznie pobierana z LDAP dla każdego użytkownika.</p>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeUserFieldForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $userFieldForm['id'] ? 'Zapisz' : 'Dodaj pole' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($subcategoryEditFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitSubcategoryEdit" class="dialog" style="max-width:520px">
|
|
<div class="dialog-title">Edytuj podkategorię</div>
|
|
<div class="field"><label>Nazwa</label><input class="input" wire:model="subcategoryEditForm.name"></div>
|
|
<div class="field"><label>Opis (opcjonalnie)</label><textarea class="input" wire:model="subcategoryEditForm.description" style="min-height:70px"></textarea></div>
|
|
<div class="field">
|
|
<label>Priorytet domyślny</label>
|
|
<select class="input" wire:model="subcategoryEditForm.defaultPriorityKey">
|
|
<option value="">— brak (użyj globalnego domyślnego) —</option>
|
|
@foreach ($this->priorities as $p)
|
|
<option value="{{ $p->key }}">{{ $p->label }}</option>
|
|
@endforeach
|
|
</select>
|
|
<span class="text-muted" style="font-size:11.5px">Priorytet nadawany automatycznie zgłoszeniom z tej podkategorii.</span>
|
|
</div>
|
|
<div class="field">
|
|
<label>Pola dodatkowe</label>
|
|
<div style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow:auto">
|
|
@foreach ($subcategoryEditForm['fieldIds'] as $i => $fieldId)
|
|
@php $field = $this->customFields->firstWhere('id', $fieldId); @endphp
|
|
@continue(! $field)
|
|
<div style="display:flex;align-items:center;gap:8px;padding:6px;background:color-mix(in srgb, var(--color-text) 3%, transparent);border-radius:4px">
|
|
<label class="radio" style="flex:1;margin:0"><input type="checkbox" checked wire:click="toggleSubcategoryEditField({{ $fieldId }})" style="position:static;opacity:1;width:auto;height:auto">{{ $field->label }}</label>
|
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryEditFieldUp({{ $fieldId }})" @disabled($i === 0) style="padding:2px;width:24px;height:24px"><span class="material-symbols-outlined" style="font-size:14px">arrow_upward</span></button>
|
|
<button class="btn btn-icon" type="button" wire:click="moveSubcategoryEditFieldDown({{ $fieldId }})" @disabled($i === count($subcategoryEditForm['fieldIds']) - 1) style="padding:2px;width:24px;height:24px"><span class="material-symbols-outlined" style="font-size:14px">arrow_downward</span></button>
|
|
</div>
|
|
@endforeach
|
|
@foreach ($this->customFields as $field)
|
|
@continue(in_array($field->id, $subcategoryEditForm['fieldIds'], true))
|
|
<div style="display:flex;align-items:center;gap:8px;padding:6px">
|
|
<label class="radio" style="flex:1;margin:0"><input type="checkbox" wire:click="toggleSubcategoryEditField({{ $field->id }})" style="position:static;opacity:1;width:auto;height:auto">{{ $field->label }}</label>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeSubcategoryEditForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">Zapisz</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($this->viewingUser)
|
|
<div class="dialog-backdrop">
|
|
<div class="dialog" style="max-width:480px;max-height:88vh;overflow:auto">
|
|
<div class="dialog-title">{{ $this->viewingUser->name }}</div>
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|
<div class="field"><label>E-mail</label><div>{{ $this->viewingUser->email }}</div></div>
|
|
<div class="field">
|
|
<label>Role</label>
|
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
@forelse ($this->viewingUser->roles ?? [] as $role)
|
|
<span class="tag tag-outline">{{ match($role) { 'client' => 'Klient', 'operator' => 'Operator', 'admin' => 'Administrator', default => $role } }}</span>
|
|
@empty
|
|
—
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label>Zespoły</label>
|
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
@forelse ($this->viewingUser->teams as $tm)
|
|
<span class="tag tag-outline">{{ $tm->name }}</span>
|
|
@empty
|
|
—
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
@if ($this->userFields->isNotEmpty())
|
|
<div style="border-top:1px solid var(--color-divider);margin-top:2px;padding-top:10px;display:flex;flex-direction:column;gap:8px">
|
|
<label style="margin:0">Pola dodatkowe</label>
|
|
@foreach ($this->userFields as $f)
|
|
@php $value = $this->viewingUser->custom_field_values[$f->id] ?? null; @endphp
|
|
<div class="field">
|
|
<label>{{ $f->label }}</label>
|
|
<div>{{ $f->type === 'checkbox' ? ($value ? 'Tak' : 'Nie') : ((string) $value !== '' && $value !== null ? $value : '—') }}</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeUserView">Zamknij</button>
|
|
<button class="btn btn-primary" type="button" wire:click="editUser({{ $this->viewingUser->id }})">Edytuj</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($userFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitUser" class="dialog" style="max-width:480px;max-height:88vh;overflow:auto">
|
|
<div class="dialog-title">{{ $userForm['id'] ? 'Edytuj użytkownika' : 'Zaproś użytkownika' }}</div>
|
|
<div class="field"><label>Imię i nazwisko</label><input class="input" wire:model="userForm.name"></div>
|
|
<div class="field"><label>E-mail</label><input class="input" type="email" wire:model="userForm.email"></div>
|
|
@error('userForm.email') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
|
|
<div style="border-top:1px solid var(--color-divider);margin-top:4px;padding-top:12px;display:flex;flex-direction:column;gap:12px">
|
|
<label style="margin:0">Konto lokalne (opcjonalnie)</label>
|
|
<p class="text-muted" style="font-size:12.5px;margin:0">Podaj hasło, aby ten użytkownik mógł logować się lokalnie, niezależnie od LDAP/AD — przydatne dla kont serwisowych lub gdy LDAP jest niedostępny.</p>
|
|
<div class="field"><label>Hasło</label><input class="input" type="password" autocomplete="new-password" placeholder="{{ $userForm['id'] ? '(bez zmian jeśli puste)' : '(pozostaw puste, aby logować się tylko przez LDAP)' }}" wire:model="userForm.password"></div>
|
|
@error('userForm.password') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
<div class="field"><label>Powtórz hasło</label><input class="input" type="password" autocomplete="new-password" wire:model="userForm.password_confirmation"></div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Role (można wybrać kilka)</label>
|
|
<div style="display:flex;gap:12px">
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked(in_array('client', $userForm['roles'])) wire:click="toggleUserFormRole('client')" style="position:static;opacity:1;width:auto;height:auto">Klient</label>
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked(in_array('operator', $userForm['roles'])) wire:click="toggleUserFormRole('operator')" style="position:static;opacity:1;width:auto;height:auto">Operator</label>
|
|
<label class="radio" style="margin:0"><input type="checkbox" @checked(in_array('admin', $userForm['roles'])) wire:click="toggleUserFormRole('admin')" style="position:static;opacity:1;width:auto;height:auto">Administrator</label>
|
|
</div>
|
|
@error('userForm.roles') <span style="color:var(--color-danger);font-size:12px">{{ $message }}</span> @enderror
|
|
</div>
|
|
|
|
@if ($userForm['id'] && $this->userFields->isNotEmpty())
|
|
<div style="border-top:1px solid var(--color-divider);margin-top:4px;padding-top:12px;display:flex;flex-direction:column;gap:12px">
|
|
<label style="margin:0">Pola dodatkowe</label>
|
|
@foreach ($this->userFields as $f)
|
|
<div class="field">
|
|
<label>{{ $f->label }}</label>
|
|
@if ($f->type === 'textarea')
|
|
<textarea class="input" wire:model="userForm.fieldValues.{{ $f->id }}"></textarea>
|
|
@elseif ($f->type === 'select')
|
|
<select class="input" wire:model="userForm.fieldValues.{{ $f->id }}">
|
|
<option value="">—</option>
|
|
@foreach ($f->options ?? [] as $opt)
|
|
<option value="{{ $opt }}">{{ $opt }}</option>
|
|
@endforeach
|
|
</select>
|
|
@elseif ($f->type === 'checkbox')
|
|
<label class="radio" style="margin:0"><input type="checkbox" wire:model="userForm.fieldValues.{{ $f->id }}" style="position:static;opacity:1;width:auto;height:auto">{{ $f->description ?: 'Tak' }}</label>
|
|
@elseif ($f->type === 'date')
|
|
<input class="input" type="date" wire:model="userForm.fieldValues.{{ $f->id }}">
|
|
@elseif ($f->type === 'number')
|
|
<input class="input" type="number" wire:model="userForm.fieldValues.{{ $f->id }}">
|
|
@else
|
|
<input class="input" type="text" wire:model="userForm.fieldValues.{{ $f->id }}">
|
|
@endif
|
|
@if ($f->description && $f->type !== 'checkbox')
|
|
<span class="text-muted" style="font-size:11.5px">{{ $f->description }}</span>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeUserForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $userForm['id'] ? 'Zapisz' : 'Wyślij zaproszenie' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($teamFormOpen)
|
|
<div class="dialog-backdrop">
|
|
<form wire:submit="submitTeam" class="dialog" style="max-width:480px">
|
|
<div class="dialog-title">{{ $teamForm['id'] ? 'Edytuj zespół' : 'Nowy zespół' }}</div>
|
|
<div class="field"><label>Nazwa zespołu</label><input class="input" wire:model="teamForm.name"></div>
|
|
<div class="field">
|
|
<label>Członkowie (operatorzy)</label>
|
|
<x-multiselect
|
|
:options="$this->operatorsForTeamForm"
|
|
:selected-ids="$teamForm['memberIds']"
|
|
toggle-action="toggleTeamMember"
|
|
placeholder="Brak wybranych członków"
|
|
/>
|
|
</div>
|
|
<div class="field">
|
|
<label>Obsługiwane podkategorie</label>
|
|
<x-multiselect
|
|
:options="$this->subcategoriesForTeamForm"
|
|
:selected-ids="$teamForm['subcategoryIds']"
|
|
toggle-action="toggleTeamSubcategory"
|
|
placeholder="Brak wybranych podkategorii"
|
|
/>
|
|
</div>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" type="button" wire:click="closeTeamForm">Anuluj</button>
|
|
<button class="btn btn-primary" type="submit">{{ $teamForm['id'] ? 'Zapisz' : 'Utwórz zespół' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($pendingDeleteType)
|
|
<div class="dialog-backdrop">
|
|
<div class="dialog" style="max-width:400px">
|
|
<div class="dialog-title">Potwierdź usunięcie</div>
|
|
<div class="dialog-body">{{ $pendingDeleteMessage }}</div>
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" wire:click="cancelPendingDelete">Anuluj</button>
|
|
<button class="btn btn-primary" wire:click="confirmPendingDelete">Usuń</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|