v1.4.0
Co nowego: - Wsparcie Active Directory dla LDAP (obok LLDAP/OpenLDAP), przełącznik typu katalogu w Admin > Integracje. - Wyszukiwarka klientów dla operatora (Operator > Klienci). - Stronicowanie kolejki operatora (50/stronę) i dashboardu klienta (20/stronę). - Globalna wyszukiwarka zgłoszeń (Ctrl+K/Cmd+K) z operatorami w stylu Gmaila (od:, temat:, treść:, numer:), plus przycisk "Szukaj" w panelu bocznym. - Ostatnio przeglądane zgłoszenia w panelu bocznym operatora. - Przeprojektowany pasek nawigacji: suwak Klient/Operator/Administrator zamiast rozwijanego menu, bogatsze menu profilu (nazwa/e-mail/role), dynamiczne tytuły kart przeglądarki na każdej podstronie. - Narzędzie do jednorazowego importu historii zgłoszeń z Heska 3.x (scripts/hesk-import/). - Poprawka: paginacja pokazywała surowe klucze tłumaczeń zamiast tekstu (brakujący lang/pl/pagination.php). Zaktualizowana dokumentacja: README, CLAUDE.md, install.md, ARCHITECTURE.md, CHANGELOG.md, wiki/*. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,63 @@ body {
|
||||
.seg-opt:has(input:checked) { background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent); }
|
||||
.seg-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||
|
||||
.panel-switch {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-text) 4%, transparent);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.panel-switch::-webkit-scrollbar { display: none; }
|
||||
.panel-switch-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--color-accent);
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
.nav .panel-switch-option {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 1 0;
|
||||
min-width: 128px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 16px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.nav .panel-switch-option:hover { color: #fff; text-decoration: none; }
|
||||
.nav .panel-switch-option-active, .nav .panel-switch-option-active:hover { color: #fff; }
|
||||
|
||||
/* Preview the destination before the click actually navigates there:
|
||||
the indicator follows whichever option is under the cursor (falling
|
||||
back to the real active position, set inline per-request by
|
||||
panel-switcher.blade.php, the moment the pointer leaves). Position is
|
||||
purely by hovered index — independent of how many roles/options
|
||||
exist — so these three rules cover the max of three areas regardless
|
||||
of which subset a given user has. */
|
||||
.panel-switch:has(> .panel-switch-option:nth-of-type(1):hover) .panel-switch-indicator { transform: translateX(0%) !important; }
|
||||
.panel-switch:has(> .panel-switch-option:nth-of-type(2):hover) .panel-switch-indicator { transform: translateX(100%) !important; }
|
||||
.panel-switch:has(> .panel-switch-option:nth-of-type(3):hover) .panel-switch-indicator { transform: translateX(200%) !important; }
|
||||
|
||||
/* The real active option's white text is only correct while the
|
||||
indicator sits under it — once hover has pulled the indicator away to
|
||||
a neighboring option, drop it back to normal text color so it doesn't
|
||||
read as near-invisible white-on-track. */
|
||||
.panel-switch:hover .panel-switch-option-active:not(:hover) { color: var(--color-text); }
|
||||
|
||||
.theme-toggle-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -341,8 +398,26 @@ body {
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-pad { padding: 16px !important; }
|
||||
.nav { padding-left: 14px !important; padding-right: 14px !important; gap: 10px; }
|
||||
.nav-panel-label { display: none; }
|
||||
.nav { padding-left: 14px !important; padding-right: 14px !important; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
/* At this width the switcher's own centered slot collides with the
|
||||
brand text and the icon buttons sharing the row (nothing left to
|
||||
shrink once labels are already at their minimum width) — same
|
||||
"restructure instead of cram" fix as the mobile table pattern above:
|
||||
drop it to its own full-width row below instead of fighting for
|
||||
space with everything else in the bar. */
|
||||
.panel-switch {
|
||||
position: relative !important;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
order: 10;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.nav .panel-switch-option { padding: 10px; font-size: 11.5px; min-width: 92px; }
|
||||
|
||||
/* Theme/notifications/profile dropdowns are anchored (position:absolute)
|
||||
to their own small trigger button by default, which overflows off the
|
||||
|
||||
50
src/resources/views/components/panel-switcher.blade.php
Normal file
50
src/resources/views/components/panel-switcher.blade.php
Normal file
@@ -0,0 +1,50 @@
|
||||
@props(['area' => null])
|
||||
|
||||
@php
|
||||
// $area is passed explicitly by each page (e.g. <x-topbar area="operator" />)
|
||||
// rather than inferred from request()->routeIs() here: this component is
|
||||
// rendered as part of each top-level Livewire page's own template, so it
|
||||
// re-renders on every wire:click/wire:model round-trip on that page (tab
|
||||
// switches, pagination, search, ...) — and during that AJAX request,
|
||||
// request()->route() is Livewire's own update route, not client./operator./
|
||||
// admin.*, which silently broke the highlight on every in-page interaction
|
||||
// when this used to key off the ambient request instead of an explicit prop.
|
||||
$user = auth()->user();
|
||||
$areas = [];
|
||||
|
||||
if ($user) {
|
||||
if ($user->isClient()) {
|
||||
$areas[] = ['label' => 'Klient', 'url' => route('client.dashboard'), 'active' => $area === 'client'];
|
||||
}
|
||||
if ($user->isOperator()) {
|
||||
$areas[] = ['label' => 'Operator', 'url' => route('operator.queue'), 'active' => $area === 'operator'];
|
||||
}
|
||||
if ($user->isAdmin()) {
|
||||
$areas[] = ['label' => 'Administrator', 'url' => route('admin.panel'), 'active' => $area === 'admin'];
|
||||
}
|
||||
}
|
||||
|
||||
$activeIndex = collect($areas)->search(fn ($area) => $area['active']);
|
||||
@endphp
|
||||
|
||||
@if (count($areas))
|
||||
<div
|
||||
class="panel-switch"
|
||||
style="position:absolute;left:50%;top:50%;transform:translate(-50%, -50%)"
|
||||
>
|
||||
@if ($activeIndex !== false)
|
||||
<span
|
||||
class="panel-switch-indicator"
|
||||
style="width:calc(100% / {{ count($areas) }});transform:translateX({{ $activeIndex * 100 }}%)"
|
||||
></span>
|
||||
@endif
|
||||
|
||||
@foreach ($areas as $item)
|
||||
<a
|
||||
href="{{ $item['url'] }}"
|
||||
wire:navigate
|
||||
class="panel-switch-option {{ $item['active'] ? 'panel-switch-option-active' : '' }}"
|
||||
>{{ $item['label'] }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@@ -1,18 +1,5 @@
|
||||
@php
|
||||
$user = auth()->user();
|
||||
$areas = [];
|
||||
|
||||
if ($user) {
|
||||
if ($user->isClient()) {
|
||||
$areas[] = ['label' => 'Panel Klienta', 'url' => route('client.dashboard'), 'active' => request()->routeIs('client.*')];
|
||||
}
|
||||
if ($user->isOperator()) {
|
||||
$areas[] = ['label' => 'Panel Operatora', 'url' => route('operator.queue'), 'active' => request()->routeIs('operator.*')];
|
||||
}
|
||||
if ($user->isAdmin()) {
|
||||
$areas[] = ['label' => 'Panel Administratora', 'url' => route('admin.panel'), 'active' => request()->routeIs('admin.*')];
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($user)
|
||||
@@ -26,23 +13,27 @@
|
||||
x-show="open"
|
||||
x-cloak
|
||||
class="nav-dropdown"
|
||||
style="position:absolute;top:100%;right:0;margin-top:6px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);min-width:200px;overflow:hidden;z-index:30"
|
||||
style="position:absolute;top:100%;right:0;margin-top:6px;background:var(--color-surface);border:1px solid var(--color-divider);border-radius:8px;box-shadow:var(--shadow-md);min-width:220px;overflow:hidden;z-index:30"
|
||||
>
|
||||
@foreach ($areas as $area)
|
||||
<a
|
||||
href="{{ $area['url'] }}"
|
||||
wire:navigate
|
||||
@click="open = false"
|
||||
class="theme-toggle-option"
|
||||
style="text-decoration:none;color:{{ $area['active'] ? 'var(--color-accent)' : 'var(--color-text)' }};font-size:12.5px"
|
||||
>{{ $area['label'] }}</a>
|
||||
@endforeach
|
||||
<div style="display:flex;flex-direction:column;gap:6px;padding:12px">
|
||||
<span style="font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $user->name }}</span>
|
||||
<span style="font-size:11.5px;color:color-mix(in srgb, var(--color-text) 60%, transparent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $user->email }}</span>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:2px">
|
||||
@if ($user->isClient())
|
||||
<span class="tag tag-neutral">Klient</span>
|
||||
@endif
|
||||
@if ($user->isOperator())
|
||||
<span class="tag tag-accent-2">Operator</span>
|
||||
@endif
|
||||
@if ($user->isAdmin())
|
||||
<span class="tag tag-accent">Administrator</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($areas))
|
||||
<div style="border-top:1px solid var(--color-divider)"></div>
|
||||
@endif
|
||||
<div style="border-top:1px solid var(--color-divider)"></div>
|
||||
|
||||
@if ($user && ($user->isOperator() || $user->isAdmin()))
|
||||
@if ($user->isOperator() || $user->isAdmin())
|
||||
<a
|
||||
href="{{ route('settings.notifications') }}"
|
||||
wire:navigate
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
@php
|
||||
$panelLabel = match (true) {
|
||||
request()->routeIs('client.*') => 'Panel Klienta',
|
||||
request()->routeIs('operator.*') => 'Panel Operatora',
|
||||
request()->routeIs('admin.*') => 'Panel Administratora',
|
||||
default => null,
|
||||
};
|
||||
@endphp
|
||||
@props(['area' => null])
|
||||
|
||||
<div class="nav" style="position:relative;padding:16px 28px;border-bottom:1px solid var(--color-divider)">
|
||||
<span class="nav-brand">{{ \App\Support\Settings::get('company_name') }}</span>
|
||||
|
||||
@if ($panelLabel)
|
||||
<span class="nav-panel-label" style="position:absolute;left:50%;top:50%;transform:translate(-50%, -50%);font-weight:500;font-size:13.5px;white-space:nowrap">{{ $panelLabel }}</span>
|
||||
@endif
|
||||
<x-panel-switcher :area="$area" />
|
||||
|
||||
<x-theme-toggle />
|
||||
|
||||
@@ -20,6 +11,7 @@
|
||||
|
||||
@auth
|
||||
<livewire:notification-bell />
|
||||
<livewire:global-search />
|
||||
@endauth
|
||||
|
||||
<x-profile-menu />
|
||||
|
||||
@@ -27,10 +27,15 @@ $tabGroups = [
|
||||
];
|
||||
@endphp
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="admin" />
|
||||
|
||||
<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))">
|
||||
<button type="button" @click="window.dispatchEvent(new CustomEvent('open-global-search'))" 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:transparent;color:var(--color-text)">
|
||||
<span class="material-symbols-outlined" style="font-size:18px">search</span>
|
||||
<span>Szukaj</span>
|
||||
</button>
|
||||
|
||||
@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>
|
||||
@@ -684,6 +689,13 @@ $tabGroups = [
|
||||
<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>Typ katalogu</label>
|
||||
<select class="input" wire:model="ldapConfig.directoryType">
|
||||
<option value="lldap">LLDAP / OpenLDAP</option>
|
||||
<option value="ad">Active Directory</option>
|
||||
</select>
|
||||
</div>
|
||||
<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>
|
||||
@@ -692,7 +704,13 @@ $tabGroups = [
|
||||
<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>
|
||||
<div class="field">
|
||||
<label>User Filter</label>
|
||||
<input class="input" placeholder="{{ $ldapConfig['directoryType'] === 'ad' ? '(sAMAccountName={0})' : '(uid={0})' }}" wire:model="ldapConfig.userFilter">
|
||||
@if ($ldapConfig['directoryType'] === 'ad')
|
||||
<p class="text-muted" style="font-size:11.5px;margin:2px 0 0">Active Directory nie ma atrybutu "uid" — puste pole domyślnie użyje sAMAccountName.</p>
|
||||
@endif
|
||||
</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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="client" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:28px;display:flex;flex-direction:column;gap:20px;max-width:920px;width:100%;margin:0 auto;box-sizing:border-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
<div class="seg">
|
||||
<label class="seg-opt"><input type="radio" name="ctab" @checked($tab === 'current') wire:click="setTab('current')">Aktualne ({{ $this->currentTickets->count() }})</label>
|
||||
<label class="seg-opt"><input type="radio" name="ctab" @checked($tab === 'archive') wire:click="setTab('archive')">Archiwalne ({{ $this->archiveTickets->count() }})</label>
|
||||
<label class="seg-opt"><input type="radio" name="ctab" @checked($tab === 'current') wire:click="setTab('current')">Aktualne ({{ $this->currentTickets->total() }})</label>
|
||||
<label class="seg-opt"><input type="radio" name="ctab" @checked($tab === 'archive') wire:click="setTab('archive')">Archiwalne ({{ $this->archiveTickets->total() }})</label>
|
||||
</div>
|
||||
<input class="input" type="search" placeholder="Szukaj po numerze, temacie, treści…" wire:model.live.debounce.400ms="search" style="max-width:280px">
|
||||
</div>
|
||||
@@ -32,6 +32,10 @@
|
||||
@if (($tab === 'current' ? $this->currentTickets : $this->archiveTickets)->isEmpty())
|
||||
<p class="text-muted" style="font-size:13px">Brak zgłoszeń w tej zakładce.</p>
|
||||
@endif
|
||||
|
||||
<div style="margin-top:4px">
|
||||
{{ ($tab === 'current' ? $this->currentTickets : $this->archiveTickets)->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="client" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:28px;display:flex;flex-direction:column;gap:20px;max-width:920px;width:100%;margin:0 auto;box-sizing:border-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="client" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:28px;display:flex;flex-direction:column;gap:20px;max-width:1180px;width:100%;margin:0 auto;box-sizing:border-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
@@ -182,13 +182,18 @@
|
||||
<div class="card" style="padding:16px;gap:8px">
|
||||
<div class="card-kicker">Inne Twoje zgłoszenia</div>
|
||||
@forelse ($this->otherTickets as $ot)
|
||||
<a href="{{ route('client.ticket', $ot) }}" wire:navigate style="display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:pointer;text-decoration:none;color:inherit">
|
||||
<a wire:key="other-ticket-{{ $ot->id }}" href="{{ route('client.ticket', $ot) }}" wire:navigate style="display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:pointer;text-decoration:none;color:inherit">
|
||||
<span style="font-size:13px">{{ $ot->displayNumber() }} — {{ $ot->subject }}</span>
|
||||
<span style="{{ $ot->statusStyle() }};flex:none">{{ $ot->statusLabel() }}</span>
|
||||
</a>
|
||||
@empty
|
||||
<p class="text-muted" style="font-size:12px;margin:0">Brak innych zgłoszeń.</p>
|
||||
<p wire:key="other-tickets-empty" class="text-muted" style="font-size:12px;margin:0">Brak innych zgłoszeń.</p>
|
||||
@endforelse
|
||||
@if (! $showAllOtherTickets && $this->otherTicketsCount > count($this->otherTickets))
|
||||
<button wire:key="show-all-other-tickets" type="button" wire:click="revealAllOtherTickets" class="btn btn-secondary" style="font-size:12px;padding:6px 10px;align-self:flex-start">
|
||||
Pokaż wszystkie ({{ $this->otherTicketsCount }})
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
51
src/resources/views/livewire/global-search.blade.php
Normal file
51
src/resources/views/livewire/global-search.blade.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
x-on:open-global-search.window="open = true; $nextTick(() => $refs.searchInput.focus())"
|
||||
x-on:keydown.cmd.k.window.prevent="open = true; $nextTick(() => $refs.searchInput.focus())"
|
||||
x-on:keydown.ctrl.k.window.prevent="open = true; $nextTick(() => $refs.searchInput.focus())"
|
||||
x-on:keydown.escape.window="open = false"
|
||||
>
|
||||
<div class="dialog-backdrop" x-show="open" x-cloak style="align-items:flex-start;padding-top:10vh">
|
||||
<div class="dialog" style="max-width:720px;padding:0;gap:0;overflow:hidden" @click.outside="open = false">
|
||||
<div style="display:flex;align-items:center;gap:14px;padding:18px 22px;border-bottom:1px solid var(--color-divider)">
|
||||
<span class="material-symbols-outlined" style="font-size:26px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">search</span>
|
||||
<input
|
||||
x-ref="searchInput"
|
||||
type="text"
|
||||
wire:model.live.debounce.200ms="search"
|
||||
placeholder="Szukaj zgłoszenia… (np. od:kacper temat:drukarka)"
|
||||
class="input"
|
||||
style="border:none;padding:10px 12px;box-shadow:none;font-size:18px"
|
||||
x-on:keydown.enter="$refs.resultsList?.querySelector('a')?.click()"
|
||||
>
|
||||
<kbd style="font-size:11px;color:color-mix(in srgb, var(--color-text) 45%, transparent);border:1px solid var(--color-divider);border-radius:4px;padding:2px 7px;flex:none">Esc</kbd>
|
||||
</div>
|
||||
|
||||
<div x-ref="resultsList" style="max-height:460px;overflow-y:auto">
|
||||
@forelse ($this->results as $ticket)
|
||||
<a
|
||||
href="{{ $this->urlFor($ticket) }}"
|
||||
wire:navigate
|
||||
@click="open = false"
|
||||
style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 22px;text-decoration:none;color:inherit;border-bottom:1px solid var(--color-divider)"
|
||||
>
|
||||
<div style="min-width:0">
|
||||
<div style="font-weight:500;font-size:14.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $ticket->displayNumber() }} — {{ $ticket->subject }}</div>
|
||||
<div class="card-meta" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $ticket->categoryLabel() }} · {{ $ticket->name }} · {{ \App\Support\Rel::format($ticket->updated_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex:none">
|
||||
<span style="{{ $ticket->priorityStyle() }}">{{ $ticket->priorityLabel() }}</span>
|
||||
<span style="{{ $ticket->statusStyle() }}">{{ $ticket->statusLabel() }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div style="padding:28px 22px;text-align:center;font-size:13.5px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">
|
||||
{{ trim($search) === '' ? 'Zacznij pisać, aby wyszukać zgłoszenie… (obsługuje też od:, temat:, treść:, numer:)' : 'Brak wyników.' }}
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar area="operator" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:20px 24px;overflow:auto;display:flex;flex-direction:column;gap:20px;max-width:820px;width:100%;margin:0 auto;box-sizing:border-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
<h2 style="margin:0">Klienci</h2>
|
||||
<a href="{{ route('operator.queue') }}" wire:navigate class="btn btn-ghost">← Wróć do listy</a>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="input"
|
||||
type="search"
|
||||
placeholder="Szukaj po imieniu, nazwisku lub adresie e-mail…"
|
||||
wire:model.live.debounce.400ms="search"
|
||||
autofocus
|
||||
>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
@if (trim($search) === '')
|
||||
<p class="text-muted" style="font-size:13px">Zacznij pisać, aby wyszukać klienta.</p>
|
||||
@else
|
||||
@forelse ($this->results as $u)
|
||||
<a
|
||||
wire:key="client-{{ $u->id }}"
|
||||
href="{{ route('operator.queue', ['filterCustomerId' => $u->id]) }}"
|
||||
wire:navigate
|
||||
class="card elev-sm"
|
||||
style="padding:14px 16px;cursor:pointer;flex-direction:row;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;text-decoration:none;color:inherit"
|
||||
>
|
||||
<div>
|
||||
<div style="font-weight:500">{{ $u->name }}</div>
|
||||
<div class="card-meta">{{ $u->email }}</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end">
|
||||
@if ($u->hasRole('client'))
|
||||
<span class="tag tag-outline">Klient</span>
|
||||
@endif
|
||||
@if ($u->hasRole('operator'))
|
||||
<span class="tag tag-outline">Operator</span>
|
||||
@endif
|
||||
@if ($u->hasRole('admin'))
|
||||
<span class="tag tag-outline">Administrator</span>
|
||||
@endif
|
||||
<span class="tag tag-neutral">{{ $u->tickets_as_customer_count }} {{ $u->tickets_as_customer_count === 1 ? 'zgłoszenie' : 'zgłoszeń' }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<p class="text-muted" style="font-size:13px">Brak wyników dla „{{ $search }}”.</p>
|
||||
@endforelse
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="operator" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:28px;display:flex;flex-direction:column;gap:20px;max-width:920px;width:100%;margin:0 auto;box-sizing:border-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="operator" />
|
||||
|
||||
<div class="app-split" style="flex:1;display:flex;min-height:0">
|
||||
<div
|
||||
@@ -14,6 +14,11 @@
|
||||
<span class="side-rail-toggle-label" x-text="collapsed ? 'Przegląd' : 'Zwiń panel'"></span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="side-rail-toggle" title="Szukaj" @click="window.dispatchEvent(new CustomEvent('open-global-search'))">
|
||||
<span class="material-symbols-outlined" style="font-size:18px">search</span>
|
||||
<span class="side-rail-toggle-label">Szukaj</span>
|
||||
</button>
|
||||
|
||||
<div class="side-rail-scroll">
|
||||
@foreach ($this->queueGroups as $groupLabel => $items)
|
||||
<div style="display:flex;flex-direction:column;gap:1px">
|
||||
@@ -27,6 +32,18 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if ($this->recentlyViewed->isNotEmpty())
|
||||
<div style="display:flex;flex-direction:column;gap:1px">
|
||||
<div class="side-rail-group-label" 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">Ostatnio przeglądane</div>
|
||||
@foreach ($this->recentlyViewed as $ticket)
|
||||
<a href="{{ route('operator.ticket', $ticket) }}" wire:navigate class="side-rail-item" title="{{ $ticket->displayNumber() }} — {{ $ticket->subject }}" style="display:flex;align-items:center;gap:10px;width:100%;padding:8px 12px;border-radius:6px;font-size:13.5px;text-align:left;white-space:nowrap;text-decoration:none;color:var(--color-text);overflow:hidden">
|
||||
<span class="material-symbols-outlined" style="font-size:18px;flex:none">history</span>
|
||||
<span class="side-rail-label" style="overflow:hidden;text-overflow:ellipsis">{{ $ticket->displayNumber() }} — {{ $ticket->subject }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +57,10 @@
|
||||
<button type="button" class="btn btn-secondary" @disabled(count($selectedIds) < 2) wire:click="mergeSelected">Scal zgłoszenia ({{ count($selectedIds) }})</button>
|
||||
<button type="button" class="btn btn-secondary" @disabled(count($selectedIds) < 1) wire:click="requestDeleteSelected" style="color:var(--color-danger)">Usuń zgłoszenia ({{ count($selectedIds) }})</button>
|
||||
</div>
|
||||
<a href="{{ route('operator.clients') }}" wire:navigate class="btn btn-secondary" style="display:flex;align-items:center;gap:6px">
|
||||
<span class="material-symbols-outlined" style="font-size:18px">person_search</span>
|
||||
Klienci
|
||||
</a>
|
||||
<a href="{{ route('operator.stats') }}" wire:navigate class="btn btn-secondary" style="display:flex;align-items:center;gap:6px">
|
||||
<span class="material-symbols-outlined" style="font-size:18px">bar_chart</span>
|
||||
Statystyki
|
||||
@@ -208,6 +229,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
{{ $this->filteredTickets->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="operator" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:20px 24px;overflow:auto;display:flex;flex-direction:column;gap:20px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="flex:1;display:flex;flex-direction:column">
|
||||
<x-topbar />
|
||||
<x-topbar area="operator" />
|
||||
|
||||
<div class="page-pad" style="flex:1;padding:20px 24px;overflow:auto">
|
||||
<div style="display:flex;flex-direction:column;gap:16px;max-width:1180px;margin:0 auto">
|
||||
|
||||
Reference in New Issue
Block a user