- Fix CI registry login (unauthorized): use dedicated REGISTRY_TOKEN secret instead of GITHUB_TOKEN, fail fast with a clear error if it's unset. - Pause ticket work-timer while a ticket is closed (won't auto-start on open or manual resume; stops on close via status change, quick action, API, or merge). - Reject empty/blank login submissions client- and server-side instead of passing them straight to the auth provider. - Closing a ticket now sends only the "ticket closed" notification instead of also sending a duplicate "status changed" one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,12 +24,19 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Verify registry credentials are configured
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "::error::Secret REGISTRY_TOKEN is not set, so login to gitea.kzbikowski.pl would fail. Create a Gitea access token with 'write:package' (and 'read:package') scope — user Settings > Applications > Generate New Token — then add it as an Actions secret named REGISTRY_TOKEN under this repo's Settings > Actions > Secrets. Aborting before attempting login." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Log in to Gitea container registry
|
- name: Log in to Gitea container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: gitea.kzbikowski.pl
|
registry: gitea.kzbikowski.pl
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -3,6 +3,29 @@
|
|||||||
All notable changes to this project are documented in this file. Format loosely
|
All notable changes to this project are documented in this file. Format loosely
|
||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-07-22
|
||||||
|
|
||||||
|
- Fixed `.gitea/workflows/build.yml`: registry login was failing with
|
||||||
|
`unauthorized` because Gitea's auto-injected `secrets.GITHUB_TOKEN` isn't
|
||||||
|
granted push access to its own container registry on this instance. Now
|
||||||
|
uses a dedicated `REGISTRY_TOKEN` secret (a Gitea access token with
|
||||||
|
`write:package`/`read:package` scope), and the workflow fails fast with a
|
||||||
|
clear `::error::` message before attempting login if that secret isn't
|
||||||
|
configured, instead of surfacing Docker's opaque `unauthorized` error.
|
||||||
|
- Fixed: opening or manually resuming a **closed** ticket no longer starts its
|
||||||
|
work timer, and closing a ticket (via the status dropdown, a reply "quick
|
||||||
|
action" transition, the REST API, or merge) now checkpoints and stops any
|
||||||
|
running timer. Time tracking only ever accrues while a ticket is open.
|
||||||
|
- Fixed: the login form accepted an empty username/password, submitting them
|
||||||
|
straight to the auth provider. The form fields are now `required` (blocks
|
||||||
|
submission client-side) and `Login::submit()` also rejects blank/
|
||||||
|
whitespace-only credentials server-side before attempting authentication,
|
||||||
|
showing "Podaj nazwę użytkownika i hasło." instead.
|
||||||
|
- Fixed: closing a ticket sent two separate notification e-mails
|
||||||
|
(`status_changed` and `ticket_closed`) for the same event. Closing now only
|
||||||
|
fires `ticket_closed`; every other status transition still fires
|
||||||
|
`status_changed` as before.
|
||||||
|
|
||||||
## [1.0.1] - 2026-07-22
|
## [1.0.1] - 2026-07-22
|
||||||
|
|
||||||
Documentation and deployment/CI overhaul — no application behavior changes.
|
Documentation and deployment/CI overhaul — no application behavior changes.
|
||||||
|
|||||||
19
install.md
19
install.md
@@ -74,7 +74,7 @@ APP_LOCALE=pl
|
|||||||
APP_FALLBACK_LOCALE=pl
|
APP_FALLBACK_LOCALE=pl
|
||||||
|
|
||||||
AUTHOR_CONTACT=helpdesk@twoja-domena.pl # widoczne w Admin > O aplikacji
|
AUTHOR_CONTACT=helpdesk@twoja-domena.pl # widoczne w Admin > O aplikacji
|
||||||
VERSION=1.0.0 # rezerwa na przyszłość, jeszcze nigdzie nie wyświetlane
|
VERSION=1.0.2 # rezerwa na przyszłość, jeszcze nigdzie nie wyświetlane
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=mariadb # nazwa serwisu z compose.yaml, NIE 127.0.0.1
|
DB_HOST=mariadb # nazwa serwisu z compose.yaml, NIE 127.0.0.1
|
||||||
@@ -136,6 +136,21 @@ kontenerów Gitea (`gitea.kzbikowski.pl/kzbkowski/servicedesk`) po każdym pushu
|
|||||||
przebudowa dla samego kodu byłaby marnowaniem czasu CI). Wypycha dwa tagi:
|
przebudowa dla samego kodu byłaby marnowaniem czasu CI). Wypycha dwa tagi:
|
||||||
`latest` i `<sha commita>`.
|
`latest` i `<sha commita>`.
|
||||||
|
|
||||||
|
Zanim to zadziała, workflow potrzebuje sekretu `REGISTRY_TOKEN` — Gitei **nie**
|
||||||
|
ufaj domyślnemu `secrets.GITHUB_TOKEN` do logowania w jej własnym rejestrze
|
||||||
|
kontenerów, w praktyce kończy się to błędem `unauthorized` przy
|
||||||
|
`docker login`. Zamiast tego:
|
||||||
|
|
||||||
|
1. Wygeneruj token: **Ustawienia użytkownika > Aplikacje > Generate New Token**,
|
||||||
|
z uprawnieniami co najmniej `write:package` i `read:package`.
|
||||||
|
2. Dodaj go jako sekret repo: **Ustawienia repo > Actions > Secrets** →
|
||||||
|
nazwa `REGISTRY_TOKEN`, wartość = wygenerowany token.
|
||||||
|
|
||||||
|
Jeśli ten sekret nie jest ustawiony, workflow celowo przerywa się **przed**
|
||||||
|
próbą logowania z czytelnym komunikatem błędu (`::error::`), zamiast wysyłać
|
||||||
|
puste/nieautoryzowane dane do rejestru i kończyć na niejasnym `unauthorized` z
|
||||||
|
demona Dockera.
|
||||||
|
|
||||||
To **tylko build + push** — świadomie bez auto-deployu na produkcję. Po tym jak
|
To **tylko build + push** — świadomie bez auto-deployu na produkcję. Po tym jak
|
||||||
CI skończy, wdrożenie nowego obrazu na serwerze wciąż jest ręcznym krokiem:
|
CI skończy, wdrożenie nowego obrazu na serwerze wciąż jest ręcznym krokiem:
|
||||||
|
|
||||||
@@ -260,7 +275,7 @@ APP_LOCALE=pl
|
|||||||
APP_FALLBACK_LOCALE=pl
|
APP_FALLBACK_LOCALE=pl
|
||||||
|
|
||||||
AUTHOR_CONTACT=helpdesk@twoja-domena.pl
|
AUTHOR_CONTACT=helpdesk@twoja-domena.pl
|
||||||
VERSION=1.0.0
|
VERSION=1.0.2
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1 # albo adres IP/hostname prawdziwego serwera DB
|
DB_HOST=127.0.0.1 # albo adres IP/hostname prawdziwego serwera DB
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ APP_DEBUG=true
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
AUTHOR_CONTACT=helpdesk@kzbikowski.pl
|
AUTHOR_CONTACT=helpdesk@kzbikowski.pl
|
||||||
VERSION=1.0.1
|
VERSION=1.0.2
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ class Login extends Component
|
|||||||
{
|
{
|
||||||
$this->error = null;
|
$this->error = null;
|
||||||
|
|
||||||
|
// The form also has HTML `required` attributes so the browser blocks
|
||||||
|
// an empty submit before it ever reaches here, but that's only a UX
|
||||||
|
// nicety — nothing stops a request hitting this method directly, so
|
||||||
|
// it needs to fail closed on its own too.
|
||||||
|
if (trim($this->username) === '' || $this->password === '') {
|
||||||
|
$this->error = 'Podaj nazwę użytkownika i hasło.';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$attribute = Settings::ldapUsernameAttribute();
|
$attribute = Settings::ldapUsernameAttribute();
|
||||||
|
|
||||||
// Local accounts (created with a password from the admin panel) don't
|
// Local accounts (created with a password from the admin panel) don't
|
||||||
|
|||||||
@@ -283,8 +283,17 @@ class Ticket extends Model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-ops on a closed ticket — time tracking only applies to open work,
|
||||||
|
* so a closed ticket's timer should never start (whether via auto-resume
|
||||||
|
* on open or the manual "Wznów" button).
|
||||||
|
*/
|
||||||
public function resumeTimer(): void
|
public function resumeTimer(): void
|
||||||
{
|
{
|
||||||
|
if ($this->isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (! $this->timer_started_at) {
|
if (! $this->timer_started_at) {
|
||||||
$this->update(['timer_started_at' => now()]);
|
$this->update(['timer_started_at' => now()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,18 @@ class TicketService
|
|||||||
$ticket->update(['status_key' => $statusKey, 'sla_notified_at' => null]);
|
$ticket->update(['status_key' => $statusKey, 'sla_notified_at' => null]);
|
||||||
$ticket->addHistory('Status zmieniony na: '.Status::labelFor($statusKey));
|
$ticket->addHistory('Status zmieniony na: '.Status::labelFor($statusKey));
|
||||||
|
|
||||||
$this->notify($ticket, 'status_changed');
|
// A transition to "closed" fires its own dedicated notification
|
||||||
|
// instead of the generic status-changed one, so closing a ticket
|
||||||
|
// doesn't send the customer/operator two emails for one event.
|
||||||
if ($statusKey === 'closed') {
|
if ($statusKey === 'closed') {
|
||||||
$this->notify($ticket, 'ticket_closed');
|
$this->notify($ticket, 'ticket_closed');
|
||||||
|
|
||||||
|
// Time tracking only applies to open work — checkpoint and pause
|
||||||
|
// the running segment (if any) the moment a ticket is closed,
|
||||||
|
// regardless of which flow triggered the status change.
|
||||||
|
$ticket->stopTimer();
|
||||||
|
} else {
|
||||||
|
$this->notify($ticket, 'status_changed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +261,7 @@ class TicketService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$other->update(['status_key' => 'closed']);
|
$other->update(['status_key' => 'closed']);
|
||||||
|
$other->stopTimer();
|
||||||
$note = $other->messages()->create([
|
$note = $other->messages()->create([
|
||||||
'author_name' => 'System',
|
'author_name' => 'System',
|
||||||
'internal' => true,
|
'internal' => true,
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Nazwa użytkownika</label>
|
<label>Nazwa użytkownika</label>
|
||||||
<input class="input" wire:model="username" autofocus>
|
<input class="input" wire:model="username" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Hasło</label>
|
<label>Hasło</label>
|
||||||
<input class="input" type="password" wire:model="password">
|
<input class="input" type="password" wire:model="password" required>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-block" type="submit">Zaloguj się</button>
|
<button class="btn btn-primary btn-block" type="submit">Zaloguj się</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -345,7 +345,9 @@
|
|||||||
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="startEditTimer">edit</span>
|
<span class="material-symbols-outlined" style="font-size:16px;cursor:pointer;opacity:0.7" wire:click="startEditTimer">edit</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:6px">
|
<div style="display:flex;gap:6px">
|
||||||
@if ($ticket->timer_started_at)
|
@if ($ticket->isClosed())
|
||||||
|
<span style="font-size:12px;opacity:0.7">Zgłoszenie zamknięte — zliczanie wstrzymane</span>
|
||||||
|
@elseif ($ticket->timer_started_at)
|
||||||
<button type="button" class="btn btn-secondary" wire:click="stopTimer" @click="running = false; clearInterval(tick)">Zatrzymaj</button>
|
<button type="button" class="btn btn-secondary" wire:click="stopTimer" @click="running = false; clearInterval(tick)">Zatrzymaj</button>
|
||||||
@else
|
@else
|
||||||
<button type="button" class="btn btn-secondary" wire:click="resumeTimer" @click="running = true; start()">Wznów</button>
|
<button type="button" class="btn btn-secondary" wire:click="resumeTimer" @click="running = true; start()">Wznów</button>
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ test('changing the subcategory fires category_changed, but re-saving details wit
|
|||||||
Notification::assertSentOnDemandTimes(TicketNotification::class, 1);
|
Notification::assertSentOnDemandTimes(TicketNotification::class, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('closing a ticket fires both status_changed and ticket_closed', function () {
|
test('closing a ticket fires only ticket_closed, not status_changed, so it does not double-notify', function () {
|
||||||
Notification::fake();
|
Notification::fake();
|
||||||
seedStatusesAndPriorities();
|
seedStatusesAndPriorities();
|
||||||
NotificationSetting::query()->where('trigger_key', 'ticket_closed')->update(['enabled' => true]);
|
NotificationSetting::query()->where('trigger_key', 'ticket_closed')->update(['enabled' => true]);
|
||||||
|
|
||||||
// status_changed ships enabled by default, but in a bare migrated (unseeded)
|
// status_changed ships enabled by default, but in a bare migrated (unseeded)
|
||||||
// database it has no template assigned yet — give it one so both triggers
|
// database it has no template assigned yet — give it one so it would have
|
||||||
// actually have something to send, isolating this test from seeding order.
|
// something to send if it (wrongly) fired, isolating this test from seeding order.
|
||||||
$statusTemplate = EmailTemplate::query()->create([
|
$statusTemplate = EmailTemplate::query()->create([
|
||||||
'key' => 'tpl-status-test', 'name' => 'Status', 'trigger_label' => 'x', 'subject' => 'S', 'body' => 'B',
|
'key' => 'tpl-status-test', 'name' => 'Status', 'trigger_label' => 'x', 'subject' => 'S', 'body' => 'B',
|
||||||
]);
|
]);
|
||||||
@@ -107,7 +107,23 @@ test('closing a ticket fires both status_changed and ticket_closed', function ()
|
|||||||
|
|
||||||
app(TicketService::class)->setStatus($ticket, 'closed');
|
app(TicketService::class)->setStatus($ticket, 'closed');
|
||||||
|
|
||||||
Notification::assertSentOnDemandTimes(TicketNotification::class, 2);
|
Notification::assertSentOnDemandTimes(TicketNotification::class, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a non-closing status change still fires status_changed as usual', function () {
|
||||||
|
Notification::fake();
|
||||||
|
seedStatusesAndPriorities();
|
||||||
|
|
||||||
|
$statusTemplate = EmailTemplate::query()->create([
|
||||||
|
'key' => 'tpl-status-test-2', 'name' => 'Status', 'trigger_label' => 'x', 'subject' => 'S', 'body' => 'B',
|
||||||
|
]);
|
||||||
|
NotificationSetting::query()->where('trigger_key', 'status_changed')->update(['email_template_id' => $statusTemplate->id]);
|
||||||
|
|
||||||
|
$ticket = makeTicket();
|
||||||
|
|
||||||
|
app(TicketService::class)->setStatus($ticket, 'open');
|
||||||
|
|
||||||
|
Notification::assertSentOnDemandTimes(TicketNotification::class, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an operator reply fires operator_replied once enabled, independent of any status change', function () {
|
test('an operator reply fires operator_replied once enabled, independent of any status change', function () {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Ldap\LldapUser;
|
use App\Ldap\LldapUser;
|
||||||
|
use App\Livewire\Auth\Login;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use LdapRecord\Laravel\Testing\DirectoryEmulator;
|
use LdapRecord\Laravel\Testing\DirectoryEmulator;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
DirectoryEmulator::tearDown();
|
DirectoryEmulator::tearDown();
|
||||||
@@ -63,3 +65,22 @@ test('an unknown username does not authenticate', function () {
|
|||||||
|
|
||||||
expect(Auth::attempt(['uid' => 'someone.else', 'password' => 'whatever']))->toBeFalse();
|
expect(Auth::attempt(['uid' => 'someone.else', 'password' => 'whatever']))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('submitting the login form with a blank username or password shows an error and never attempts to authenticate', function () {
|
||||||
|
Livewire::test(Login::class)
|
||||||
|
->set('username', '')
|
||||||
|
->set('password', '')
|
||||||
|
->call('submit')
|
||||||
|
->assertSet('error', 'Podaj nazwę użytkownika i hasło.');
|
||||||
|
|
||||||
|
expect(Auth::check())->toBeFalse();
|
||||||
|
|
||||||
|
// Whitespace-only counts as blank for the username too.
|
||||||
|
Livewire::test(Login::class)
|
||||||
|
->set('username', ' ')
|
||||||
|
->set('password', 'somepassword')
|
||||||
|
->call('submit')
|
||||||
|
->assertSet('error', 'Podaj nazwę użytkownika i hasło.');
|
||||||
|
|
||||||
|
expect(Auth::check())->toBeFalse();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Livewire\Operator\TicketShow as OperatorTicketShow;
|
use App\Livewire\Operator\TicketShow as OperatorTicketShow;
|
||||||
|
use App\Services\TicketService;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
test('opening a ticket for the first time auto-starts the timer', function () {
|
test('opening a ticket for the first time auto-starts the timer', function () {
|
||||||
@@ -186,6 +187,47 @@ test('the stop-timer beacon endpoint checkpoints and stops a running timer', fun
|
|||||||
->and($ticket->time_spent_seconds)->toBe(50);
|
->and($ticket->time_spent_seconds)->toBe(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('opening a closed ticket does not auto-start the timer', function () {
|
||||||
|
seedStatusesAndPriorities();
|
||||||
|
$operator = operatorUser('timer-closed-open@example.com');
|
||||||
|
$ticket = makeTicket(['status_key' => 'closed', 'time_spent_seconds' => 30, 'timer_started_at' => null]);
|
||||||
|
|
||||||
|
Livewire::actingAs($operator)->test(OperatorTicketShow::class, ['ticket' => $ticket]);
|
||||||
|
|
||||||
|
$ticket->refresh();
|
||||||
|
expect($ticket->timer_started_at)->toBeNull()
|
||||||
|
->and($ticket->time_spent_seconds)->toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manually resuming a closed ticket does not start the timer', function () {
|
||||||
|
seedStatusesAndPriorities();
|
||||||
|
$operator = operatorUser('timer-closed-resume@example.com');
|
||||||
|
$ticket = makeTicket(['status_key' => 'closed', 'timer_started_at' => null]);
|
||||||
|
|
||||||
|
Livewire::actingAs($operator)->test(OperatorTicketShow::class, ['ticket' => $ticket])
|
||||||
|
->call('resumeTimer');
|
||||||
|
|
||||||
|
expect($ticket->fresh()->timer_started_at)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closing a ticket via TicketService::setStatus checkpoints and stops a running timer', function () {
|
||||||
|
seedStatusesAndPriorities();
|
||||||
|
$this->travelTo(now());
|
||||||
|
$operator = operatorUser('timer-close-via-status@example.com');
|
||||||
|
$ticket = makeTicket();
|
||||||
|
|
||||||
|
Livewire::actingAs($operator)->test(OperatorTicketShow::class, ['ticket' => $ticket]);
|
||||||
|
$ticket->refresh();
|
||||||
|
|
||||||
|
$this->travel(70)->seconds();
|
||||||
|
|
||||||
|
app(TicketService::class)->setStatus($ticket, 'closed');
|
||||||
|
|
||||||
|
$ticket->refresh();
|
||||||
|
expect($ticket->timer_started_at)->toBeNull()
|
||||||
|
->and($ticket->time_spent_seconds)->toBe(70);
|
||||||
|
});
|
||||||
|
|
||||||
test('cancelling the timer edit leaves the tracked time untouched', function () {
|
test('cancelling the timer edit leaves the tracked time untouched', function () {
|
||||||
seedStatusesAndPriorities();
|
seedStatusesAndPriorities();
|
||||||
$operator = operatorUser('timer-edit-cancel@example.com');
|
$operator = operatorUser('timer-edit-cancel@example.com');
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ więcej informacji”, „Restart usuwa problem”.
|
|||||||
odpowiedział, SLA przekroczone) — każde ma przełącznik włącz/wyłącz, odbiorcę
|
odpowiedział, SLA przekroczone) — każde ma przełącznik włącz/wyłącz, odbiorcę
|
||||||
(klient / operator) i przypisany szablon. Usunięcie przypisanego szablonu po
|
(klient / operator) i przypisany szablon. Usunięcie przypisanego szablonu po
|
||||||
prostu wyłącza wysyłkę tego powiadomienia, dopóki ktoś nie wybierze nowego.
|
prostu wyłącza wysyłkę tego powiadomienia, dopóki ktoś nie wybierze nowego.
|
||||||
|
„Zmiana statusu” i „zgłoszenie zamknięte” się wzajemnie wykluczają dla tej
|
||||||
|
samej zmiany — zamknięcie zgłoszenia wysyła wyłącznie powiadomienie
|
||||||
|
„zgłoszenie zamknięte”, żeby nie dublować maila.
|
||||||
|
|
||||||
## Wygląd / Branding
|
## Wygląd / Branding
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ W widoku pojedynczego zgłoszenia:
|
|||||||
- **Załączniki** — do odpowiedzi/notatki, w granicach limitów ustawionych przez
|
- **Załączniki** — do odpowiedzi/notatki, w granicach limitów ustawionych przez
|
||||||
administratora.
|
administratora.
|
||||||
- **Licznik czasu pracy** — start/stop/reset przy zgłoszeniu; czas zapisuje się
|
- **Licznik czasu pracy** — start/stop/reset przy zgłoszeniu; czas zapisuje się
|
||||||
automatycznie nawet przy zamknięciu karty (mechanizm `sendBeacon`).
|
automatycznie nawet przy zamknięciu karty (mechanizm `sendBeacon`). Zliczanie
|
||||||
|
jest automatycznie wstrzymywane, gdy zgłoszenie ma status zamknięty — nie
|
||||||
|
uruchomi się przy otwarciu zamkniętego zgłoszenia ani nie będzie dalej biec
|
||||||
|
po jego zamknięciu; wcześniej naliczony czas można wciąż ręcznie skorygować.
|
||||||
- **Edycja danych zgłoszenia** — temat, opis, podkategoria, pola dodatkowe;
|
- **Edycja danych zgłoszenia** — temat, opis, podkategoria, pola dodatkowe;
|
||||||
zmiana kategorii może wysłać powiadomienie do klienta.
|
zmiana kategorii może wysłać powiadomienie do klienta.
|
||||||
- **Historia** — log każdej zmiany (status, priorytet, zespół, przypisanie) z
|
- **Historia** — log każdej zmiany (status, priorytet, zespół, przypisanie) z
|
||||||
|
|||||||
Reference in New Issue
Block a user