Obsah ticketu se na detailu cte, ne jen vypisuje
body je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil. Krok
"Zalozit nebo doplnit ticket" dosadi {{data}} a sablona objekt prevede na JSON,
takze v obsahu skoncila slozena zavorka vypsana jako odstavec. Necitelne, i kdyz
jsou v ni presne ty udaje, kvuli kterym ticket vznikl.
Nova web/src/components/dashboard/TicketBody.tsx telo prectе:
- veta od cloveka -> text tak, jak prisel
- cely retezec je JSON -> tabulka klic a hodnota
- seznam objektu -> tabulka se sloupci podle klicu
- text a za nim JSON -> odstavec a pod nim tabulka
- cokoliv, co se neprecte -> text tak, jak prisel
Ctvrta radka je bezny pripad, sablona {{voicebotId}}, {{data}} da presne tohle.
Deli se az od prvni zavorky, od ktere je zbytek platny JSON - bez te druhe
podminky by veta s jednou slozenou zavorkou skoncila rozpulena.
Zanoreni se kresli o uroven niz jako vnorena tabulka, hloubeji uz jako JSON:
tabulka v tabulce v tabulce se necte a odesilatele umi zanorit data hluboko.
Nic se nezahazuje, neprecteny obsah se ukaze jako puvodni text.
Overeno na osmi tvarech tela vcetne toho, ktery na instanci opravdu vznika,
vety se slozenou zavorkou a nedopsaneho JSONu.
Vedle toho zdroje vizualniho navrhu detailu ticketu v design/. Sestavene platno
je v gitignore, je to 2,5 MB generovaneho souboru, ktery vznikne znovu ze
zdroju vedle nej.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
40875439e4
commit
a6722f9379
@@ -19,3 +19,7 @@ Thumbs.db
|
||||
# Lomitko na zacatku je nutne: bez nej by se vzorec shodl s KAZDOU
|
||||
# slozkou 'data', tedy i se src/data - a zdrojove soubory by tise chybely.
|
||||
/data/
|
||||
|
||||
# Sestavene platno navrhu. 2,5 MB generovaneho souboru, ktery vznikne znovu
|
||||
# ze zdroju vedle nej (design/*.dc.html a canvas.json).
|
||||
design/detail-ticketu.html
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
body { margin: 0; background: #0f1411; font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; }
|
||||
a { color: #cdff7a; text-decoration: none; }
|
||||
a:hover { color: #b7ff3c; }
|
||||
.h { font-family: 'Archivo', ui-sans-serif, system-ui, sans-serif; letter-spacing: -0.035em; line-height: 1.04; }
|
||||
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
|
||||
.tab { display: inline-flex; align-items: center; gap: 9px; border-radius: 12px 12px 0 0; border: 1px solid rgba(46,58,50,0.7); border-bottom: none; background: rgba(15,20,17,0.6); padding: 9px 18px 10px; font-family: inherit; font-size: 13.5px; font-weight: 500; color: rgba(255,255,255,0.5); cursor: pointer; }
|
||||
.tab:hover { color: #ffffff; background-color: rgba(26,33,28,0.7); }
|
||||
.tab-on { background: rgba(26,33,28,0.9); color: #ffffff; }
|
||||
.num { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; background: rgba(255,255,255,0.08); font-size: 11px; color: rgba(255,255,255,0.5); }
|
||||
.lbl { font-size: 12px; color: rgba(255,255,255,0.35); }
|
||||
.val { font-size: 13.5px; color: rgba(255,255,255,0.8); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<div style="width: 1180px; min-height: 1180px; box-sizing: border-box; background: #0f1411; padding: 36px 48px 52px; color: #ffffff; display: flex; flex-direction: column; gap: 18px;">
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 24px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255,255,255,0.4);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
<span>Tickety</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<button style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; border-radius: 999px; background-color: rgba(26,33,28,0.7); border: 1px solid rgba(46,58,50,0.7); color: #ffffff; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
Vyřídit
|
||||
</button>
|
||||
<button style="display: inline-flex; align-items: center; justify-content: center; height: 36px; padding: 0 18px; border: none; border-radius: 999px; background: #b7ff3c; color: #080b09; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: 0 10px 15px -3px rgba(155,230,25,0.2);">Převzít</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SLOZKA s rozradovaci -->
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
|
||||
<div style="display: flex; align-items: flex-end; gap: 5px; padding-left: 24px;">
|
||||
<button class="tab tab-on" onClick="{{ openPozadavek }}" style="{{ styPozadavek }}">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#cdff7a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
<span class="mono" style="letter-spacing: 0.04em;">TK-4823</span>
|
||||
</button>
|
||||
<button class="tab" onClick="{{ openLog }}" style="{{ styLog }}">Log průběhu <span class="num">12</span></button>
|
||||
<button class="tab" onClick="{{ openUdalosti }}" style="{{ styUdalosti }}">Události <span class="num">4</span></button>
|
||||
<button class="tab" onClick="{{ openPole }}" style="{{ styPole }}">Vlastní pole <span class="num">3</span></button>
|
||||
</div>
|
||||
|
||||
<div style="border-radius: 0 16px 16px 16px; border: 1px solid rgba(46,58,50,0.7); background: rgba(26,33,28,0.7); padding: 30px 34px 34px;">
|
||||
|
||||
<!-- LIST: pozadavek -->
|
||||
<sc-if value="{{ isPozadavek }}" hint-placeholder-val="{{ true }}">
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; align-items: start;">
|
||||
|
||||
<div style="grid-column: span 2; display: flex; flex-direction: column; gap: 26px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<h1 class="h" style="margin: 0; font-size: 26px; font-weight: 800; color: #ffffff; text-wrap: pretty;">Prádelna v Brně neodpovídá na platbu kartou</h1>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: rgba(255,255,255,0.6);">
|
||||
Přišlo z hlasové linky přes automatizaci <a href="#">TEST</a>, u nás od 1. září.
|
||||
Zákazník od té doby nepsal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- OBSAH: telo se cte. Text zustane textem, struktura se vypise jako tabulka. -->
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 16px;">
|
||||
<span style="font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.45);">Obsah</span>
|
||||
<span style="font-size: 12px; color: rgba(255,255,255,0.3);">přišlo jako data, ne jako věta</span>
|
||||
</div>
|
||||
|
||||
<p style="margin: 0; font-size: 15.5px; line-height: 1.65; color: rgba(255,255,255,0.85);">
|
||||
Hovor byl přesměrován na živého operátora, problém se týkal reklamace platby.
|
||||
</p>
|
||||
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.7); overflow: hidden;">
|
||||
<div style="display: flex; border-bottom: 1px solid rgba(46,58,50,0.4);">
|
||||
<div style="width: 33%; padding: 10px 14px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.45);">result</div>
|
||||
<div style="flex-grow: 1; padding: 10px 14px; font-size: 14px; color: rgba(255,255,255,0.78);">Chybějící informace</div>
|
||||
</div>
|
||||
<div style="display: flex; border-bottom: 1px solid rgba(46,58,50,0.4);">
|
||||
<div style="width: 33%; padding: 10px 14px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.45);">Místo</div>
|
||||
<div style="flex-grow: 1; padding: 10px 14px; font-size: 14px; color: rgba(255,255,255,0.78);">Brno, Veveří</div>
|
||||
</div>
|
||||
<div style="display: flex; border-bottom: 1px solid rgba(46,58,50,0.4);">
|
||||
<div style="width: 33%; padding: 10px 14px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.45);">Chybějící informace</div>
|
||||
<div style="flex-grow: 1; padding: 10px 14px; font-size: 14px; color: rgba(255,255,255,0.78);">Číslo transakce a čas platby</div>
|
||||
</div>
|
||||
<div style="display: flex; border-bottom: 1px solid rgba(46,58,50,0.4);">
|
||||
<div style="width: 33%; padding: 10px 14px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.45);">rating</div>
|
||||
<div style="flex-grow: 1; padding: 10px 14px; font-size: 14px; line-height: 1.5; color: rgba(255,255,255,0.78);">Hovor byl zahájen správným zjištěním lokality, ale operátor nezískal číslo transakce.</div>
|
||||
</div>
|
||||
<div style="display: flex;">
|
||||
<div style="width: 33%; padding: 10px 14px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.45);">questionsRelevance</div>
|
||||
<div class="mono" style="flex-grow: 1; padding: 10px 14px; font-size: 14px; color: rgba(255,255,255,0.78);">3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<span style="font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.45);">Co k tomu přibylo</span>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); border-left: 3px solid rgba(183,255,60,0.5); background: rgba(15,20,17,0.55); padding: 18px 22px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 8px;">
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Petr Vomáčka odpověděl zákazníkovi</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.3);">1. 9., 15:03</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.6; color: rgba(255,255,255,0.75);">
|
||||
Ověřujeme transakci u provozovatele terminálu, ozveme se do dvou pracovních dnů.
|
||||
</p>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 2px;">
|
||||
<span style="width: 6px; height: 6px; border-radius: 999px; background: rgba(255,255,255,0.25);"></span>
|
||||
<span style="font-size: 13.5px; color: rgba(255,255,255,0.45);">Petr Vomáčka si ticket převzal</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.28);">dnes, 8:12</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px; border-top: 1px solid rgba(46,58,50,0.5); padding-top: 22px;">
|
||||
<div style="flex-grow: 1; border-radius: 12px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.7); padding: 11px 14px;">
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.25);">Odpovědět zákazníkovi nebo přidat interní poznámku...</span>
|
||||
</div>
|
||||
<button style="display: inline-flex; align-items: center; justify-content: center; height: 40px; padding: 0 20px; border: none; border-radius: 999px; background: #b7ff3c; color: #080b09; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer;">Odeslat</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STITEK NA SLOZCE -->
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); background: rgba(15,20,17,0.55); overflow: hidden;">
|
||||
<div style="padding: 16px 18px; border-bottom: 1px solid rgba(46,58,50,0.5); display: flex; flex-direction: column; gap: 12px;">
|
||||
<span class="lbl" style="letter-spacing: 0.06em; text-transform: uppercase;">Má u sebe</span>
|
||||
<div style="display: flex; align-items: center; gap: 11px;">
|
||||
<span class="mono" style="display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 999px; background: rgba(155,230,25,0.12); border: 1px solid rgba(183,255,60,0.35); color: #cdff7a; font-size: 13px;">PV</span>
|
||||
<div style="display: flex; flex-direction: column; gap: 1px; min-width: 0;">
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Petr Vomáčka</span>
|
||||
<span style="font-size: 12.5px; color: rgba(255,255,255,0.4);">Servicedesk · 7 otevřených</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px 18px; border-bottom: 1px solid rgba(46,58,50,0.5); display: flex; flex-direction: column; gap: 11px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span class="lbl">Stav, jak ho poslal voicebot</span>
|
||||
<span style="display: inline-flex; align-self: flex-start; align-items: center; border-radius: 999px; border: 1px solid rgba(251,191,36,0.35); background: rgba(245,158,11,0.12); padding: 4px 10px; font-size: 12px; font-weight: 500; color: #fbbf24;">Chybějící informace</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Vyřízený</span>
|
||||
<span class="val">ne</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Priorita</span>
|
||||
<span style="font-size: 13.5px; color: #fb7185;">vysoká</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px 18px; border-bottom: 1px solid rgba(46,58,50,0.5); display: flex; flex-direction: column; gap: 11px;">
|
||||
<span class="lbl" style="letter-spacing: 0.06em; text-transform: uppercase;">Zákazník</span>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Firma</span>
|
||||
<span class="val" style="text-align: right;">CSBot</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Kontakt</span>
|
||||
<span class="val" style="text-align: right;">Jan Dvořák</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Odpověď na</span>
|
||||
<span class="mono val" style="text-align: right; font-size: 12.5px;">+420 604 838 600</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px 18px; display: flex; flex-direction: column; gap: 11px;">
|
||||
<span class="lbl" style="letter-spacing: 0.06em; text-transform: uppercase;">Jak to jde</span>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">První reakce</span>
|
||||
<span style="font-size: 13.5px; color: #2dd4bf;">za 36 minut</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Otevřeno</span>
|
||||
<span class="val">18 hodin</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px;">
|
||||
<span class="lbl">Vráceno do práce</span>
|
||||
<span style="font-size: 13.5px; color: #fbbf24;">2x</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; border-top: 1px solid rgba(46,58,50,0.4); padding-top: 11px;">
|
||||
<span class="lbl">Externí ID</span>
|
||||
<span class="mono" style="font-size: 11.5px; color: rgba(255,255,255,0.5); text-align: right; word-break: break-all;">CAbc75a8e8c9c3<br>a17ed530d152dbf5acdb</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- LIST: log prubehu -->
|
||||
<sc-if value="{{ isLog }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 24px;">
|
||||
<h2 class="h" style="margin: 0; font-size: 20px; font-weight: 700; color: #ffffff;">Log průběhu</h2>
|
||||
<span style="font-size: 13px; color: rgba(255,255,255,0.35);">12 kroků · celkem 214 ms · automatizace TEST</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px; border-radius: 10px; background: rgba(155,230,25,0.06);">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#cdff7a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><path d="M4 4h16v12H5.17L4 17.17Z"/></svg>
|
||||
<div style="display: flex; flex-direction: column; gap: 2px; flex-grow: 1;">
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Přijata událost: stav completed</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.35);">1. 9. 14:27:16</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px 11px 42px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#2dd4bf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-grow: 1;">
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.85);">ticket/upsert · založen TK-4823</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.3);">18 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px 11px 42px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<div style="display: flex; flex-direction: column; gap: 3px; flex-grow: 1;">
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.85);">Podmínka: výsledek není "Chybějící informace"</span>
|
||||
<span style="font-size: 12.5px; color: rgba(255,255,255,0.4);">větev NE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px 11px 70px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#2dd4bf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<div style="display: flex; flex-direction: column; gap: 7px; flex-grow: 1; min-width: 0;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.85);">ticket/upsert · doplněn TK-4823, priorita vysoká</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.3);">6 ms</span>
|
||||
</div>
|
||||
<pre class="mono" style="margin: 0; border-radius: 8px; background: rgba(8,11,9,0.6); border: 1px solid rgba(46,58,50,0.5); padding: 11px 13px; font-size: 12px; line-height: 1.55; color: rgba(255,255,255,0.55); white-space: pre-wrap;">{ "ticketId": "TK-4823", "created": false,
|
||||
"externalId": "CAbc75a8e8c9c3a17ed530d152dbf5acdb" }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px 11px 70px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fb7185" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
|
||||
<div style="display: flex; flex-direction: column; gap: 7px; flex-grow: 1; min-width: 0;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 14px; color: #fb7185;">ticket/assign-group · skupina nenalezena</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.3);">3 ms</span>
|
||||
</div>
|
||||
<p style="margin: 0; border-left: 2px solid rgba(251,113,133,0.4); padding-left: 12px; font-size: 13px; line-height: 1.5; color: rgba(255,255,255,0.6);">
|
||||
Skupina grp_reklamace ve firmě neexistuje. Ticket zůstal ve frontě bez řešitele.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- LIST: prijate udalosti -->
|
||||
<sc-if value="{{ isUdalosti }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 24px;">
|
||||
<h2 class="h" style="margin: 0; font-size: 20px; font-weight: 700; color: #ffffff;">Přijaté události</h2>
|
||||
<span style="font-size: 13px; color: rgba(255,255,255,0.35);">Co přišlo zvenku, v původní podobě</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); background: rgba(15,20,17,0.55); padding: 18px 20px; display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">stav completed</span>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="display: inline-flex; align-items: center; border-radius: 999px; border: 1px solid rgba(251,191,36,0.35); background: rgba(245,158,11,0.12); padding: 3px 9px; font-size: 11.5px; font-weight: 500; color: #fbbf24;">3x totéž</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.35);">1. 9. 14:27:27</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="mono" style="margin: 0; border-radius: 8px; background: rgba(8,11,9,0.6); border: 1px solid rgba(46,58,50,0.5); padding: 13px 15px; font-size: 12px; line-height: 1.6; color: rgba(255,255,255,0.6); white-space: pre-wrap;">{
|
||||
"callSid": "CAbc75a8e8c9c3a17ed530d152dbf5acdb",
|
||||
"status": "completed",
|
||||
"voicebotId": "dffe761f-cc56-4f3b-923a-70fd14cb4e91",
|
||||
"data": { "result": "Chybějící informace", "rating": "Hovor byl zahájen..." }
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); background: rgba(15,20,17,0.4); padding: 15px 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.7);">stav in-progress</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.35);">1. 9. 14:27:16</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
|
||||
<!-- LIST: vlastni pole -->
|
||||
<sc-if value="{{ isPole }}" hint-placeholder-val="{{ false }}">
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 24px;">
|
||||
<h2 class="h" style="margin: 0; font-size: 20px; font-weight: 700; color: #ffffff;">Vlastní pole</h2>
|
||||
<span style="font-size: 13px; color: rgba(255,255,255,0.35);">Typ ticketu Hovor · doplní se i další zprávou</span>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||||
<span class="lbl">Místo</span>
|
||||
<div style="border-radius: 10px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.7); padding: 10px 13px; font-size: 14px; color: #ffffff;">Brno, Veveří</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||||
<span class="lbl">Přesměrování</span>
|
||||
<div style="border-radius: 10px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.7); padding: 10px 13px; font-size: 14px; color: #ffffff;">+420 604 838 600</div>
|
||||
</div>
|
||||
<div style="grid-column: span 2; display: flex; flex-direction: column; gap: 6px;">
|
||||
<span class="lbl">Chybějící informace</span>
|
||||
<div style="border-radius: 10px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.7); padding: 10px 13px; font-size: 14px; color: #ffffff;">Číslo transakce a čas platby</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end;">
|
||||
<button style="display: inline-flex; align-items: center; justify-content: center; height: 36px; padding: 0 18px; border: none; border-radius: 999px; background: #b7ff3c; color: #080b09; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer;">Uložit</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
|
||||
<script data-dc-script data-props='{"$preview":{"width":1180,"height":1180}}'>
|
||||
class Component extends DCLogic {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Ktery list ve slozce je navrchu.
|
||||
this.state = { list: 'pozadavek' };
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const on = this.state.list;
|
||||
// Aktivni rozradovac splyva s telem slozky. Pocita se tady, protoze
|
||||
// sablona neumi vyraz - do stylu se smi dosadit jen hotova hodnota.
|
||||
const sty = (key) =>
|
||||
on === key
|
||||
? 'background-color: rgba(26,33,28,0.9); color: #ffffff;'
|
||||
: 'background-color: rgba(15,20,17,0.6); color: rgba(255,255,255,0.5);';
|
||||
|
||||
return {
|
||||
isPozadavek: on === 'pozadavek',
|
||||
isLog: on === 'log',
|
||||
isUdalosti: on === 'udalosti',
|
||||
isPole: on === 'pole',
|
||||
styPozadavek: sty('pozadavek'),
|
||||
styLog: sty('log'),
|
||||
styUdalosti: sty('udalosti'),
|
||||
styPole: sty('pole'),
|
||||
openPozadavek: () => this.setState({ list: 'pozadavek' }),
|
||||
openLog: () => this.setState({ list: 'log' }),
|
||||
openUdalosti: () => this.setState({ list: 'udalosti' }),
|
||||
openPole: () => this.setState({ list: 'pole' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
body { margin: 0; background: #0f1411; font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; }
|
||||
a { color: #cdff7a; text-decoration: none; }
|
||||
a:hover { color: #b7ff3c; }
|
||||
.h { font-family: 'Archivo', ui-sans-serif, system-ui, sans-serif; letter-spacing: -0.035em; line-height: 1.04; }
|
||||
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<div style="width: 900px; min-height: 1080px; box-sizing: border-box; background: #0f1411; padding: 40px 60px 56px; color: #ffffff; display: flex; flex-direction: column; gap: 20px;">
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255,255,255,0.4);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
<span>Moje požadavky</span>
|
||||
</div>
|
||||
|
||||
<!-- SLOZKA: jazyk nahore, pod nim jedna karta -->
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
|
||||
<!-- jazyk slozky -->
|
||||
<div style="display: flex; align-items: flex-end; gap: 6px; padding-left: 24px;">
|
||||
<div style="display: inline-flex; align-items: center; gap: 10px; border-radius: 12px 12px 0 0; border: 1px solid rgba(46,58,50,0.7); border-bottom: none; background: rgba(26,33,28,0.9); padding: 9px 18px 10px;">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#cdff7a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
<span class="mono" style="font-size: 13px; letter-spacing: 0.04em; color: rgba(255,255,255,0.75);">TK-4823</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- telo slozky -->
|
||||
<div style="border-radius: 0 16px 16px 16px; border: 1px solid rgba(46,58,50,0.7); background: rgba(26,33,28,0.7); padding: 32px 36px 36px; display: flex; flex-direction: column; gap: 28px;">
|
||||
|
||||
<!-- co to je a jak to je, jednou vetou -->
|
||||
<div style="display: flex; flex-direction: column; gap: 14px;">
|
||||
<h1 class="h" style="margin: 0; font-size: 27px; font-weight: 800; color: #ffffff; text-wrap: pretty;">Prádelna v Brně neodpovídá na platbu kartou</h1>
|
||||
<p style="margin: 0; font-size: 16px; line-height: 1.55; color: rgba(255,255,255,0.7);">
|
||||
Máme to u sebe od 1. září. Pracuje na tom <span style="color: #ffffff; font-weight: 600;">Petr Vomáčka</span>
|
||||
a naposledy se vám ozval včera odpoledne.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- stitek na slozce: jen to, co je opravdu v datech -->
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0; border-radius: 12px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.55); overflow: hidden;">
|
||||
<div style="flex-grow: 1; min-width: 150px; padding: 14px 18px; border-right: 1px solid rgba(46,58,50,0.5); display: flex; flex-direction: column; gap: 4px;">
|
||||
<span style="font-size: 12px; color: rgba(255,255,255,0.35);">Přijali jsme</span>
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.85);">1. 9. ve 14:27</span>
|
||||
</div>
|
||||
<div style="flex-grow: 1; min-width: 150px; padding: 14px 18px; border-right: 1px solid rgba(46,58,50,0.5); display: flex; flex-direction: column; gap: 4px;">
|
||||
<span style="font-size: 12px; color: rgba(255,255,255,0.35);">Ozvali jsme se</span>
|
||||
<span style="font-size: 14px; color: #2dd4bf;">za 36 minut</span>
|
||||
</div>
|
||||
<div style="flex-grow: 1; min-width: 150px; padding: 14px 18px; display: flex; flex-direction: column; gap: 4px;">
|
||||
<span style="font-size: 12px; color: rgba(255,255,255,0.35);">Hotovo</span>
|
||||
<span style="font-size: 14px; color: rgba(255,255,255,0.5);">zatím ne</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- prvni list ve slozce: co poslal zakaznik -->
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<span style="font-size: 13px; font-weight: 600; letter-spacing: 0.02em; color: rgba(255,255,255,0.45);">Co jste nám poslali</span>
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); background: rgba(15,20,17,0.55); padding: 20px 22px;">
|
||||
<p style="margin: 0; font-size: 15.5px; line-height: 1.65; color: rgba(255,255,255,0.85); text-wrap: pretty;">
|
||||
Volal jsem kvůli prádelně v Brně na Veveří. Platba kartou neprošla, terminál napsal chybu a peníze
|
||||
mi odešly z účtu. Potřebuju vědět, jestli se vrátí, nebo mám reklamovat u banky.
|
||||
</p>
|
||||
<span class="mono" style="display: block; margin-top: 12px; font-size: 12px; color: rgba(255,255,255,0.3);">1. 9. 2026, 14:27 · telefon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- dalsi listy: co pribylo, jako papiry ve slozce -->
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<span style="font-size: 13px; font-weight: 600; letter-spacing: 0.02em; color: rgba(255,255,255,0.45);">Co k tomu přibylo</span>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.6); border-left: 3px solid rgba(183,255,60,0.5); background: rgba(15,20,17,0.55); padding: 18px 22px;">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 8px;">
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Petr Vomáčka</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.3);">1. 9., 15:03</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.6; color: rgba(255,255,255,0.75);">
|
||||
Dobrý den, ověřujeme transakci u provozovatele terminálu. Ozveme se nejpozději do dvou pracovních dnů.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 4px 4px 4px 2px;">
|
||||
<span style="width: 6px; height: 6px; border-radius: 999px; background: rgba(255,255,255,0.25);"></span>
|
||||
<span style="font-size: 13.5px; color: rgba(255,255,255,0.45);">Požadavek si převzal Petr Vomáčka</span>
|
||||
<span class="mono" style="font-size: 12px; color: rgba(255,255,255,0.28);">dnes, 8:12</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- co muze zakaznik udelat: jedna vec -->
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; border-top: 1px solid rgba(46,58,50,0.5); padding-top: 24px;">
|
||||
<span style="font-size: 13px; font-weight: 600; letter-spacing: 0.02em; color: rgba(255,255,255,0.45);">Chcete něco doplnit?</span>
|
||||
<div style="border-radius: 12px; border: 1px solid rgba(46,58,50,0.7); background: rgba(15,20,17,0.7); padding: 14px 16px; min-height: 76px;">
|
||||
<span style="font-size: 14.5px; color: rgba(255,255,255,0.25);">Napište nám. Přečte to člověk, který požadavek řeší.</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 16px;">
|
||||
<span style="font-size: 13px; color: rgba(255,255,255,0.35);">Odpovídáme v pracovní dny do 24 hodin.</span>
|
||||
<button style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 44px; padding: 0 24px; border: none; border-radius: 999px; background: #b7ff3c; color: #080b09; font-family: inherit; font-size: 15.2px; font-weight: 600; cursor: pointer; box-shadow: 0 10px 15px -3px rgba(155,230,25,0.2);">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
|
||||
Odeslat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="margin: 0; padding-left: 4px; font-size: 13px; color: rgba(255,255,255,0.3);">
|
||||
Číslo požadavku <span class="mono">TK-4823</span> se hodí, když nám budete volat.
|
||||
</p>
|
||||
</div>
|
||||
</x-dc>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"artboards": [
|
||||
{ "file": "Zakaznik.dc.html", "title": "Zákazník", "x": 0, "y": 0, "w": 900, "h": 1080 },
|
||||
{ "file": "Main.dc.html", "title": "Řešitel a admin", "x": 1020, "y": 0, "w": 1180, "h": 1180 }
|
||||
],
|
||||
"annotations": [
|
||||
{
|
||||
"id": "note-zakaznik",
|
||||
"x": 0,
|
||||
"y": -170,
|
||||
"w": 460,
|
||||
"text": "ZÁKAZNÍK\nJedna složka, jeden list. Žádné kroky stavu - ty se z volného řetězce poskládat nedají.\nŠtítek nese jen to, co v datech opravdu je: kdy přišlo, kdy jsme se ozvali, jestli je hotovo."
|
||||
},
|
||||
{
|
||||
"id": "note-admin",
|
||||
"x": 1020,
|
||||
"y": -170,
|
||||
"w": 540,
|
||||
"text": "ŘEŠITEL A ADMIN\nTáž složka, jen má rozřaďovače. Log, události a pole jsou další listy uvnitř, ne modaly.\nKlikni na jazyky nahoře - přepínají se.\nStav je vypsaný tak, jak ho poslal odesílatel, a vedle něj zvlášť příznak Vyřízený."
|
||||
},
|
||||
{
|
||||
"id": "note-stav",
|
||||
"x": 1620,
|
||||
"y": -170,
|
||||
"w": 400,
|
||||
"text": "PROČ ŽÁDNÉ KROKY\nstatus je volný řetězec, cokoliv pošle cizí aplikace.\nSpolehlivě se dá odvodit jen: kdy přišlo (createdAt), kdy jsme reagovali (firstResponseAt) a jestli je hotovo (closed).\nPřesně to je na štítku."
|
||||
},
|
||||
{
|
||||
"id": "note-obsah",
|
||||
"x": 1020,
|
||||
"y": 1230,
|
||||
"w": 620,
|
||||
"text": "OBSAH SE ČTE\nTělo ticketu je jeden řetězec, ale co v něm stojí, určuje ten, kdo ho naplnil.\nVěta od člověka zůstane větou (viz složka zákazníka vlevo). Struktura z automatizace se vypíše jako tabulka.\nA na tenhle případ - text a za ním objekt, protože šablona zní {{voicebotId}}, {{data}} - se rozdělí na obojí.\nHotové, běží to v aplikaci."
|
||||
}
|
||||
],
|
||||
"launch": { "view": "canvas" }
|
||||
}
|
||||
@@ -64,6 +64,33 @@ to napise nahlas misto toho, aby ukazal prazdne misto.
|
||||
skoro vsechno ostatni - podrobnosti nize v "Zalozeni a doplneni delaji totez".
|
||||
Cela historie zustava v udalostech ticketu, at uz je v `body` cokoliv.
|
||||
|
||||
### Obsah se na detailu cte, ne jen vypisuje
|
||||
|
||||
`body` je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil. Casto
|
||||
je to veta od cloveka, casto ale cela struktura z cizi aplikace: krok dosadi
|
||||
`{{data}}` a sablona objekt prevede na JSON. Vypsat takovou zavorku jako
|
||||
odstavec je necitelne, i kdyz jsou v ni presne ty udaje, kvuli kterym ticket
|
||||
vznikl.
|
||||
|
||||
`TicketBody` proto telo prectе a podle toho vykresli:
|
||||
|
||||
| Co v tele je | Jak se to ukaze |
|
||||
| ------------------------- | -------------------------------------- |
|
||||
| veta od cloveka | text tak, jak prisel |
|
||||
| cely retezec je JSON | tabulka klic a hodnota |
|
||||
| seznam objektu | tabulka se sloupci podle klicu |
|
||||
| text a za nim JSON | odstavec a pod nim tabulka |
|
||||
| cokoliv, co se neprecte | text tak, jak prisel |
|
||||
|
||||
Ta ctvrta radka je bezny pripad: sablona `{{voicebotId}}, {{data}}` da presne
|
||||
tohle. Deli se **az od prvni zavorky, od ktere je zbytek platny JSON** - jinak
|
||||
by veta s jednou slozenou zavorkou skoncila rozpulena.
|
||||
|
||||
Zanoreni se kresli o uroven niz jako vnorena tabulka, hloubeji uz jako JSON.
|
||||
Tabulka v tabulce v tabulce se necte a odesilatele umi zanorit data hluboko.
|
||||
|
||||
**Nic se nezahazuje.** Kdyz se struktura precist neda, ukaze se puvodni text.
|
||||
|
||||
Uvnitr ulozista se drzi jen `assigneeId`, jmeno se dopocitava pri cteni. Kdyz
|
||||
resitel ze seznamu zmizi, ticket nespadne - jen se zaloguje a tvari se jako
|
||||
neprirazeny.
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-02 - Obsah ticketu se na detailu cte
|
||||
|
||||
`body` je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil. Krok
|
||||
"Zalozit nebo doplnit ticket" dosadi `{{data}}` a sablona objekt prevede na
|
||||
JSON, takze v obsahu skoncila slozena zavorka vypsana jako odstavec. Necitelne,
|
||||
i kdyz jsou v ni presne ty udaje, kvuli kterym ticket vznikl.
|
||||
|
||||
Nova `web/src/components/dashboard/TicketBody.tsx` telo prectе:
|
||||
|
||||
| Co v tele je | Jak se to ukaze |
|
||||
| ----------------------- | ------------------------------ |
|
||||
| veta od cloveka | text tak, jak prisel |
|
||||
| cely retezec je JSON | tabulka klic a hodnota |
|
||||
| seznam objektu | tabulka se sloupci podle klicu |
|
||||
| text a za nim JSON | odstavec a pod nim tabulka |
|
||||
| cokoliv, co se neprecte | text tak, jak prisel |
|
||||
|
||||
Ctvrta radka je bezny pripad: sablona `{{voicebotId}}, {{data}}` da presne
|
||||
tohle. Deli se az od **prvni zavorky, od ktere je zbytek platny JSON** - bez te
|
||||
druhe podminky by veta s jednou slozenou zavorkou skoncila rozpulena.
|
||||
|
||||
Zanoreni se kresli o uroven niz jako vnorena tabulka, hloubeji uz jako JSON:
|
||||
tabulka v tabulce v tabulce se necte a odesilatele umi zanorit data hluboko.
|
||||
Nic se nezahazuje, neprecteny obsah se ukaze jako puvodni text.
|
||||
|
||||
Overeno na osmi tvarech tela vcetne toho, ktery na instanci opravdu vznika,
|
||||
vety se slozenou zavorkou a nedopsaneho JSONu.
|
||||
|
||||
## 2026-09-02 - U posledniho volani je videt, co presne prislo
|
||||
|
||||
Prvni verze seznamu poslednich volani ukazovala cas a duvod odmitnuti, ale ne
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Obsah ticketu.
|
||||
*
|
||||
* `body` je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil.
|
||||
* Casto je to veta od cloveka, casto ale i cela struktura z cizi aplikace:
|
||||
* krok "Zalozit nebo doplnit ticket" dosadi `{{data}}` a sablona objekt
|
||||
* prevede na JSON. Vypsat takovou zavorku jako odstavec je pro cloveka
|
||||
* necitelne, i kdyz jsou v ni presne ty udaje, kvuli kterym ticket vznikl.
|
||||
*
|
||||
* Proto se telo **cte**, ne jen vypisuje:
|
||||
* - cely retezec je JSON -> tabulka,
|
||||
* - text a za nim JSON -> odstavec a pod nim tabulka (`{{voicebotId}}, {{data}}`),
|
||||
* - cokoliv jineho -> text tak, jak prisel.
|
||||
*
|
||||
* Nic se nezahazuje: kdyz se struktura precist neda, ukaze se puvodni text.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/** Co v tele stoji. `text` je vzdycky to, co ma zustat cist jako veta. */
|
||||
interface ParsedBody {
|
||||
text: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rozdeli telo na text a strukturu.
|
||||
*
|
||||
* Hleda se **prvni zavorka, od ktere je zbytek platny JSON**. Bez te druhe
|
||||
* podminky by staciloa jedna slozena zavorka v bezne vete a text by se rozpadl;
|
||||
* takhle se rozdeli jen to, co se opravdu rozparsuje.
|
||||
*/
|
||||
export function parseBody(body: string): ParsedBody {
|
||||
const trimmed = body.trim();
|
||||
if (trimmed === '') return { text: '', data: undefined };
|
||||
|
||||
const whole = tryParse(trimmed);
|
||||
if (whole !== undefined) return { text: '', data: whole };
|
||||
|
||||
const start = trimmed.search(/[[{]/);
|
||||
if (start > 0) {
|
||||
const tail = tryParse(trimmed.slice(start));
|
||||
if (tail !== undefined) {
|
||||
// Oddelovac pred strukturou uz nic nenese, casto je to carka ze sablony.
|
||||
return { text: trimmed.slice(0, start).replace(/[\s,;:-]+$/, ''), data: tail };
|
||||
}
|
||||
}
|
||||
|
||||
return { text: trimmed, data: undefined };
|
||||
}
|
||||
|
||||
/** `undefined` = nerozparsovalo se, nebo to neni struktura. */
|
||||
function tryParse(text: string): unknown {
|
||||
if (!/^[[{]/.test(text)) return undefined;
|
||||
try {
|
||||
const value: unknown = JSON.parse(text);
|
||||
return value !== null && typeof value === 'object' ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function TicketBody({ body, sourceRef }: { body: string; sourceRef?: string | null }) {
|
||||
const { text, data } = parseBody(body);
|
||||
|
||||
if (text === '' && data === undefined) {
|
||||
return (
|
||||
<p className="mt-3 rounded-lg border border-dashed border-ink-600/70 px-3 py-2.5 text-xs text-white/40">
|
||||
Prázdné. Krok "Založit ticket" neměl nastavené pole Obsah, takže se text původního
|
||||
požadavku nikam neuložil.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-3">
|
||||
{text !== '' && (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap text-white/75">{text}</p>
|
||||
)}
|
||||
{data !== undefined && <DataTable value={data} />}
|
||||
{sourceRef && <p className="font-mono text-xs text-white/30">zdroj {sourceRef}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Struktura jako tabulka.
|
||||
*
|
||||
* Seznam objektu dostane sloupce podle klicu, protoze radky se v nem
|
||||
* 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 }) {
|
||||
if (Array.isArray(value) && value.length > 0 && value.every(isPlainObject)) {
|
||||
const columns = [...new Set(value.flatMap((row) => Object.keys(row)))];
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-ink-600/70">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-ink-600/70 bg-ink-900/40">
|
||||
{columns.map((column) => (
|
||||
<th key={column} className="px-3 py-2 text-left font-medium text-white/45">
|
||||
{column}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{value.map((row, index) => (
|
||||
<tr key={index} className="border-b border-ink-600/40 last:border-0">
|
||||
{columns.map((column) => (
|
||||
<td key={column} className="px-3 py-2 align-top text-white/75">
|
||||
<Cell value={(row as Record<string, unknown>)[column]} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rows: Array<[string, unknown]> = Array.isArray(value)
|
||||
? value.map((item, index) => [`${index + 1}.`, item])
|
||||
: Object.entries(value as Record<string, unknown>);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <p className="text-xs text-white/35">Struktura bez hodnot.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-ink-600/70">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{rows.map(([key, item]) => (
|
||||
<tr key={key} className="border-b border-ink-600/40 last:border-0">
|
||||
{/* Klic je casto cesky primo od odesilatele, nic se neprekladá. */}
|
||||
<th className="w-1/3 px-3 py-2 text-left align-top font-medium text-white/45">
|
||||
{key}
|
||||
</th>
|
||||
<td className="px-3 py-2 align-top text-white/75">
|
||||
<Cell value={item} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedna hodnota.
|
||||
*
|
||||
* Zanorena struktura se vykresli znovu jako tabulka, ale **jen o uroven niz**.
|
||||
* Hlubsi zanoreni se ukaze jako JSON: tabulka v tabulce v tabulce uz se necte
|
||||
* a odesilatele umi poslat data zanorena hodne hluboko.
|
||||
*/
|
||||
function Cell({ value, depth = 0 }: { value: unknown; depth?: number }): ReactNode {
|
||||
if (value === null || value === undefined) return <span className="text-white/25">-</span>;
|
||||
|
||||
if (typeof value === 'boolean') return value ? 'ano' : 'ne';
|
||||
if (typeof value === 'number') return <span className="font-mono">{value}</span>;
|
||||
if (typeof value === 'string') {
|
||||
if (value.trim() === '') return <span className="text-white/25">-</span>;
|
||||
return <span className="whitespace-pre-wrap">{value}</span>;
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.every((item) => typeof item !== 'object' || item === null)) {
|
||||
return value.map((item) => String(item)).join(', ');
|
||||
}
|
||||
|
||||
if (depth === 0) return <NestedTable value={value} />;
|
||||
|
||||
return (
|
||||
<pre className="overflow-x-auto font-mono text-xs whitespace-pre-wrap text-white/55">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
/** Zanorena struktura. Bez ramecku, aby vnorena tabulka nedelala matriosku. */
|
||||
function NestedTable({ value }: { value: object }) {
|
||||
const rows: Array<[string, unknown]> = Array.isArray(value)
|
||||
? value.map((item, index) => [`${index + 1}.`, item])
|
||||
: Object.entries(value as Record<string, unknown>);
|
||||
|
||||
if (rows.length === 0) return <span className="text-white/25">-</span>;
|
||||
|
||||
return (
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{rows.map(([key, item]) => (
|
||||
<tr key={key}>
|
||||
<th className="w-1/3 py-1 pr-3 text-left align-top font-normal text-white/35">{key}</th>
|
||||
<td className="py-1 align-top text-white/70">
|
||||
<Cell value={item} depth={1} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TicketStatusBadge,
|
||||
} from '@/components/dashboard/StatusBadge';
|
||||
import { TicketActions } from '@/components/dashboard/TicketActions';
|
||||
import { TicketBody } from '@/components/dashboard/TicketBody';
|
||||
import { TicketEvents } from '@/components/dashboard/TicketEvents';
|
||||
import { TicketTrace, TraceSummary } from '@/components/dashboard/TicketTrace';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
@@ -199,21 +200,7 @@ export default function TicketDetail() {
|
||||
<FileText className="size-4 text-brand-300" />
|
||||
Obsah požadavku
|
||||
</h2>
|
||||
{ticket.data.body.trim().length > 0 ? (
|
||||
<p className="mt-3 text-sm leading-relaxed whitespace-pre-wrap text-white/75">
|
||||
{ticket.data.body}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 rounded-lg border border-dashed border-ink-600/70 px-3 py-2.5 text-xs text-white/40">
|
||||
Prázdné. Krok "Založit ticket" neměl nastavené pole Obsah, takže se text
|
||||
původního požadavku nikam neuložil.
|
||||
</p>
|
||||
)}
|
||||
{ticket.data.sourceRef && (
|
||||
<p className="mt-3 font-mono text-xs text-white/30">
|
||||
zdroj {ticket.data.sourceRef}
|
||||
</p>
|
||||
)}
|
||||
<TicketBody body={ticket.data.body} sourceRef={ticket.data.sourceRef} />
|
||||
</section>
|
||||
|
||||
<section className="glass rounded-card p-5 sm:p-6">
|
||||
|
||||
Reference in New Issue
Block a user