Detail ticketu ma rozradovace, ne tri karty pod sebou

Navrh v documentation/25 mel detail jako jednu slozku s rozradovaci, ale
v aplikaci zustaly tri karty pod sebou: obsah, prichozi udalosti a log prubehu.
Detail byl tri obrazovky dlouhy a to podstatne, tedy obsah pozadavku, se v tom
ztracelo.

Ted jsou to listy jedne karty:

- Pozadavek - obsah a pole na komentar, vychozi
- Log prubehu - co delala automatizace a co ji sluzby vratily
- Udalosti - co presne prislo zvenku
- Vlastni pole - hodnoty poli typu ticketu

U kazdeho je pocet, takze je videt, jestli se tam vubec vyplati kliknout.
Vlastni pole se nenabizeji, kdyz zadna nejsou - prazdny rozradovac je jen dalsi
vec k prehlednuti.

Vychozi je vzdycky Pozadavek: to je duvod, proc clovek ticket otevrel. Log
a udalosti jsou hloubkova data, na ta se sahá, az kdyz neco nesedi.

Vlastni pole se do te doby nedala zobrazit vubec, i kdyz je ticket nesl - to je
ten ctvrty rozradovac. Kresli je tatáz tabulka jako obsah ticketu (DataTable
v TicketBody.tsx), takze se struktura vsude vypisuje stejne.

Rozradovace pouzivaji tentyz vzhled zalozek jako zbytek portalu, ne tvar slozky
z navrhu. Druhy styl zalozek na jedne obrazovce je presne ten drift, ktery jsme
uklizeli u vstupnich poli.

Overeno na bezici instanci: ticket z automatizace ma 5 kroku logu, 2 udalosti
a zadna vlastni pole, takze se ctvrty rozradovac skryje.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-02 12:29:18 +02:00
co-authored by Claude Opus 5
parent 6bcd7f845c
commit 2a310137a2
3 changed files with 178 additions and 67 deletions
+31
View File
@@ -2,6 +2,37 @@
Nejnovejsi nahore.
## 2026-09-02 - Detail ticketu ma rozradovace, ne tri karty pod sebou
Navrh v [25](25-navrh-pristupny-portal.md) mel detail jako jednu slozku
s rozradovaci, ale v aplikaci zustaly tri karty pod sebou: obsah, prichozi
udalosti a log prubehu. Detail byl tri obrazovky dlouhy a to podstatne, tedy
obsah pozadavku, se v tom ztracelo.
Ted jsou to listy jedne karty:
| Rozradovac | Co je na nem |
| ------------ | ------------------------------------------------- |
| Požadavek | obsah a pole na komentar, vychozi |
| Log průběhu | co delala automatizace a co ji sluzby vratily |
| Události | co presne prislo zvenku |
| Vlastní pole | hodnoty poli typu ticketu |
U kazdeho je pocet, takze je videt, jestli se tam vubec vyplati kliknout.
**Vlastni pole se nenabizeji, kdyz zadna nejsou** - prazdny rozradovac je jen
dalsi vec k prehlednuti.
Vychozi je vzdycky Pozadavek: to je duvod, proc clovek ticket otevrel. Log
a udalosti jsou hloubkova data, na ta se sahá, az kdyz neco nesedi.
Vlastni pole se do te doby nedala zobrazit vubec, i kdyz je ticket nesl - to
je ten ctvrty rozradovac. Kresli je tatáz tabulka jako obsah ticketu
(`DataTable` v `TicketBody.tsx`), takze se struktura vsude vypisuje stejne.
Rozradovace pouzivaji **tentyz vzhled zalozek jako zbytek portalu**, ne tvar
slozky z navrhu. Druhy styl zalozek na jedne obrazovce je presne ten drift,
ktery jsme uklizeli u vstupnich poli.
## 2026-09-02 - Hierarchie firmy: kdo co vidi, a zalozka Firma
Pohled na celou firmu dostal kazdy, kdo do ni patril: `scopes.push('tenant')`
+1 -1
View File
@@ -113,7 +113,7 @@ export function TicketBody({ body, sourceRef }: { body: string; sourceRef?: stri
* porovnavaji mezi sebou. Vsechno ostatni je dvojice klic a hodnota - u jedne
* struktury nikdo nesrovnava sloupce, cte se shora dolu.
*/
function DataTable({ value }: { value: unknown }) {
export function DataTable({ value }: { value: unknown }) {
if (Array.isArray(value) && value.length > 0 && value.every(isPlainObject)) {
const columns = [...new Set(value.flatMap((row) => Object.keys(row)))];
return (
+146 -66
View File
@@ -18,7 +18,7 @@ import {
TicketStatusBadge,
} from '@/components/dashboard/StatusBadge';
import { TicketActions } from '@/components/dashboard/TicketActions';
import { TicketBody } from '@/components/dashboard/TicketBody';
import { DataTable, TicketBody } from '@/components/dashboard/TicketBody';
import { TicketEvents } from '@/components/dashboard/TicketEvents';
import { TicketTrace, TraceSummary } from '@/components/dashboard/TicketTrace';
import { Badge } from '@/components/ui/Badge';
@@ -98,6 +98,34 @@ export default function TicketDetail() {
*/
const [statusDraft, setStatusDraft] = useState<string | null>(null);
/**
* Ktery list detailu je navrchu.
*
* Vychozi je vzdycky pozadavek: to je duvod, proc clovek ticket otevrel.
* Log a udalosti jsou hloubkova data, na ta se sahá, az kdyz neco nesedi.
*/
const [activeSheet, setSheet] = useState('pozadavek');
const fieldCount = Object.keys(ticket.data?.fields ?? {}).length;
/** Rozradovace. Vlastni pole se nenabizeji, kdyz zadna nejsou. */
const sheets = [
{ key: 'pozadavek', label: 'Požadavek', count: undefined, show: true },
{
key: 'log',
label: 'Log průběhu',
count: ticket.data?.trace.length ?? 0,
show: true,
},
{
key: 'udalosti',
label: 'Události',
count: ticket.data?.events.length ?? 0,
show: true,
},
{ key: 'pole', label: 'Vlastní pole', count: fieldCount, show: fieldCount > 0 },
].filter((sheet) => sheet.show);
usePageMeta({ title: `${ticket.data ? ticket.data.id : 'Ticket'}`, area: 'portal' });
// Po zmene ticketu zmizi stara chyba, at nevisi u uz opraveneho stavu.
@@ -194,74 +222,126 @@ export default function TicketDetail() {
<TicketActions ticket={ticket.data} onDone={ticket.reload} />
<div className="grid gap-6 xl:grid-cols-[1fr_20rem]">
<div className="space-y-6">
<section className="glass rounded-card p-5 sm:p-6">
<h2 className="flex items-center gap-2 font-semibold text-white">
<FileText className="size-4 text-brand-300" />
Obsah požadavku
</h2>
<div className="mt-3">
<TicketBody body={ticket.data.body} sourceRef={ticket.data.sourceRef} />
</div>
</section>
<section className="glass rounded-card p-5 sm:p-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="flex items-center gap-2 font-semibold text-white">
<Inbox className="size-4 text-brand-300" />
Příchozí události
</h2>
<span className="text-xs text-white/35">
{ticket.data.events.length}
{ticket.data.externalId
? ` k externímu ID ${ticket.data.externalId}`
: ''}
</span>
</div>
<TicketEvents events={ticket.data.events} />
</section>
<section className="glass rounded-card p-5 sm:p-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="flex items-center gap-2 font-semibold text-white">
<ScrollText className="size-4 text-brand-300" />
Průběh a log
</h2>
<TraceSummary entries={ticket.data.trace} />
{/*
Rozradovace misto tri karet pod sebou. Log prubehu a prichozi
udalosti jsou hloubkova data: dulezita, ale ne pokazde. Kdyz
stály na strance vsechny naraz, byl detail tri obrazovky dlouhy
a to podstatne, tedy obsah pozadavku, se v tom ztracelo.
*/}
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{sheets.map((sheet) => (
<button
key={sheet.key}
type="button"
onClick={() => setSheet(sheet.key)}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
sheet.key === activeSheet
? 'bg-brand-500/15 text-brand-100'
: 'text-white/50 hover:text-white',
)}
>
{sheet.label}
{sheet.count !== undefined && (
<span className="rounded-full bg-white/8 px-1.5 text-xs text-white/50">
{sheet.count}
</span>
)}
</button>
))}
</div>
<TicketTrace
entries={ticket.data.trace}
services={catalog.data?.items ?? []}
/>
<section className="glass rounded-card p-5 sm:p-6">
{activeSheet === 'pozadavek' && (
<>
<h2 className="flex items-center gap-2 font-semibold text-white">
<FileText className="size-4 text-brand-300" />
Obsah požadavku
</h2>
<div className="mt-3">
<TicketBody body={ticket.data.body} sourceRef={ticket.data.sourceRef} />
</div>
<form
onSubmit={(event) => void submitComment(event)}
className="mt-5 border-t border-ink-600/50 pt-5"
>
<label
htmlFor="ticket-comment"
className="text-xs font-semibold tracking-wide text-white/45 uppercase"
>
Přidat komentář
</label>
<div className="mt-2 flex flex-wrap gap-2">
<input
id="ticket-comment"
value={comment}
onChange={(event) => setComment(event.target.value)}
placeholder="Co jste zjistili nebo udělali..."
className="min-w-0 flex-1 rounded-xl border border-ink-600/70 bg-ink-850/70 px-3.5 py-2.5 text-sm text-white placeholder:text-white/30 focus:border-brand-400/70 focus:outline-none"
/>
<Button type="submit" size="sm" disabled={busy || comment.trim().length < 2}>
<MessageSquarePlus className="size-4" />
Zapsat
</Button>
</div>
<p className="mt-2 text-xs text-white/35">
Komentář se zapíše do stejné časové osy jako běh automatizace.
</p>
</form>
<form
onSubmit={(event) => void submitComment(event)}
className="mt-5 border-t border-ink-600/50 pt-5"
>
<label
htmlFor="ticket-comment"
className="text-xs font-semibold tracking-wide text-white/45 uppercase"
>
Přidat komentář
</label>
<div className="mt-2 flex flex-wrap gap-2">
<input
id="ticket-comment"
value={comment}
onChange={(event) => setComment(event.target.value)}
placeholder="Co jste zjistili nebo udělali..."
className="min-w-0 flex-1 rounded-xl border border-ink-600/70 bg-ink-850/70 px-3.5 py-2.5 text-sm text-white placeholder:text-white/30 focus:border-brand-400/70 focus:outline-none"
/>
<Button
type="submit"
size="sm"
disabled={busy || comment.trim().length < 2}
>
<MessageSquarePlus className="size-4" />
Zapsat
</Button>
</div>
<p className="mt-2 text-xs text-white/35">
Komentář se zapíše do stejné časové osy jako běh automatizace.
</p>
</form>
</>
)}
{activeSheet === 'log' && (
<>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="flex items-center gap-2 font-semibold text-white">
<ScrollText className="size-4 text-brand-300" />
Průběh a log
</h2>
<TraceSummary entries={ticket.data.trace} />
</div>
<TicketTrace
entries={ticket.data.trace}
services={catalog.data?.items ?? []}
/>
</>
)}
{activeSheet === 'udalosti' && (
<>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="flex items-center gap-2 font-semibold text-white">
<Inbox className="size-4 text-brand-300" />
Příchozí události
</h2>
<span className="text-xs text-white/35">
{ticket.data.externalId
? `k externímu ID ${ticket.data.externalId}`
: ''}
</span>
</div>
<TicketEvents events={ticket.data.events} />
</>
)}
{activeSheet === 'pole' && (
<>
<h2 className="flex items-center gap-2 font-semibold text-white">
<Inbox className="size-4 text-brand-300" />
Vlastní pole typu
</h2>
<p className="mt-1 mb-3 text-xs text-white/35">
Klíče se sčítají, další zpráva o téže věci je jen doplní.
</p>
<DataTable value={ticket.data.fields} />
</>
)}
</section>
</div>