Add ticket status setter and prevent draft text loss

- Set ticket state (Offen / In Bearbeitung / Warten auf Schließen /
  Geschlossen) from the ticket detail page via a live PUT to Zammad;
  pending-close gets a default pending time and the change is synced back
  so it isn't flagged as an external change
- Replace the disruptive meta-refresh with a JS auto-refresh that pauses
  while a text field has unsent content or focus
- Persist reply/new-ticket drafts to localStorage and restore them after
  a reload or accidental navigation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 11:15:30 +02:00
co-authored by Claude Opus 4.8
parent 1041f283a4
commit 1107a912bc
7 changed files with 141 additions and 11 deletions
+22 -5
View File
@@ -126,24 +126,41 @@ class ZammadClient:
r = self.session.get(f"{self.base}/api/v1/signshow", timeout=30)
return r.headers.get("CSRF-TOKEN")
def _post(self, path: str, payload: dict, _retry: bool = True):
def _write(self, method: str, path: str, payload: dict, _retry: bool = True):
self.ensure_auth()
headers = {}
if not config.ZAMMAD_TOKEN:
csrf = self._csrf()
if csrf:
headers["X-CSRF-Token"] = csrf
r = self.session.post(f"{self.base}{path}", json=payload,
headers=headers, timeout=60)
r = self.session.request(method, f"{self.base}{path}", json=payload,
headers=headers, timeout=60)
if r.status_code in (401, 403) and _retry and not config.ZAMMAD_TOKEN:
log.warning("Auth/CSRF abgelaufen (HTTP %s), erneuter Login.", r.status_code)
self._authed = False
self._login_session()
return self._post(path, payload, _retry=False)
return self._write(method, path, payload, _retry=False)
if r.status_code not in (200, 201):
raise ZammadError(f"POST {path} -> HTTP {r.status_code}: {r.text[:300]}")
raise ZammadError(f"{method} {path} -> HTTP {r.status_code}: {r.text[:300]}")
return r.json()
def _post(self, path: str, payload: dict, _retry: bool = True):
return self._write("POST", path, payload, _retry)
def _put(self, path: str, payload: dict, _retry: bool = True):
return self._write("PUT", path, payload, _retry)
def ticket_states(self) -> list[dict]:
return self._get("/api/v1/ticket_states")
def set_state(self, ticket_id: int, state_id: int,
pending_time: str | None = None) -> dict:
"""Status eines Tickets setzen (an EPI wirksam)."""
payload = {"state_id": state_id}
if pending_time:
payload["pending_time"] = pending_time
return self._put(f"/api/v1/tickets/{ticket_id}", payload)
def create_article(self, ticket_id: int, body: str,
attachments: list[dict] | None = None) -> dict:
"""Antwort/Nachricht an ein bestehendes Ticket anhängen (an EPI sichtbar)."""