Tickety maji zalozky, fronta umi prirazovat
Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi ostatnimi filtry - splyvaly s nimi a vypadaly jako dalsi dva chipy. Jenze "co mam u sebe", "co jeste nikdo nema" a "co je ve firme" nejsou filtry, jsou to tri ruzne prace a kazda chce jinou tabulku. Tri zalozky: - Moje tickety, pro toho, kdo je veden jako resitel. Bez sloupce Resi, byl by tam porad on - Nezarazene, pro kazdeho. Radky s rychlym prirazenim - Vsechny tickety, jen pro toho, kdo vidi i cizi. Sloupec Resi a filtr na sekce Sloupec Resi je jen ve Vsech: v Mych by ve vsech radcich stalo tyz jmeno a ve fronte je z definice prazdny. Az v prehledu vsech ma smysl videt, kdo co ma u sebe. Zalozka Vsechny se nenabizi tomu, kdo vidi jen svoje - byl by to tentyz seznam. Rika to Access.seesOthers, a Access.visibleGroups k tomu prida sekce, ktere smi filtrovat: kdo vidi celou firmu vsechny, vedouci jen ty, ktere vede. Nova QuickAssign: u kazdeho ticketu ve fronte tlacitko Vzit si a dve roletky, komu a do ktere sekce. Prirazeni je ve fronte hlavni prace, ne jedna z mnoha veci na detailu - otevirat kvuli tomu kazdy ticket znamena u dvaceti ticketech ctyricet kliknuti navic. Dve roletky, ne jedna: clovek a sekce jsou dve ruzna rozhodnuti. Kdo nesmi rozdavat praci ostatnim, vidi jen Vzit si. Endpointy uz existovaly, nova je jen cesta k nim. Vychozi zalozka se odvodi z adresy: odkaz z widgetu nese assignee, takze unassigned otevre frontu a konkretni resitel nebo sekce pohled Vsechny. Bez toho by clovek prisel z dlazdice "fronta bez resitele" a koukal na svoje. Do navrhu 25 zapsano, ze hledani ma zapadnout do zalozky, ve ktere clovek stoji, ne ji obejit, a vysledek se ma vracet do te same zalozky. Overeno na bezici instanci: spravce platformy ma zalozku Vsechny a vsechny sekce ve filtru, Vomacka jako vedouci Servicedesku jen svoji sekci, Kriz jako radovy clen zalozku Vsechny nema. A cely pohyb ticketu frontou: admin preda TK-4819 do sekce, Kriz ho vidi ve fronte, vezme si ho a objevi se mu v Mych ticketech. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
52999a961b
commit
e701bc0e2c
@@ -287,6 +287,20 @@ prilezitost prestat posilat vsechno.
|
||||
|
||||
### Tvar
|
||||
|
||||
### Hledani zna zalozku, ve ktere stoji
|
||||
|
||||
Seznam ticketu ma tri zalozky: Moje, Nezarazene a Vsechny (posledni jen pro
|
||||
toho, kdo vidi i cizi tickety). Hledani do nich musi zapadnout, ne je obejit:
|
||||
|
||||
- **z Mych hleda ve svych**, z Nezarazenych ve fronte, z Vsech v celem rozsahu
|
||||
stropu. Modal to ma nahore napsat, ne to delat potichu.
|
||||
- **volba \"hledat vsude\"** dava smysl jen tam, kde je zalozka Vsechny. Kdo
|
||||
vidi jen svoje, nema kam rozsirit.
|
||||
- vysledek se vraci **do te same zalozky**, ze ktere clovek hledal.
|
||||
|
||||
Bez toho by hledani bylo ctvrta zalozka, ktera si zaklada vlastni pravidla -
|
||||
a clovek by po zavreni nalezu nevedel, kde vlastne je.
|
||||
|
||||
**Modal je jen zadani.** Kriteria:
|
||||
|
||||
- text a k nemu volba kde (predmet, obsah, externi ID, kontakt, vse),
|
||||
|
||||
@@ -2,6 +2,71 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-02 - Tickety maji zalozky, fronta umi prirazovat
|
||||
|
||||
Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi
|
||||
ostatnimi filtry - splyvaly s nimi a vypadaly jako dalsi dva chipy. Jenze "co
|
||||
mam u sebe", "co jeste nikdo nema" a "co je ve firme" nejsou filtry, jsou to
|
||||
**tri ruzne prace a kazda chce jinou tabulku**.
|
||||
|
||||
### Tri zalozky
|
||||
|
||||
| Zalozka | Kdo ji ma | Cim se lisi |
|
||||
| ---------------- | ----------------------------- | ------------------------------------ |
|
||||
| Moje tickety | kdo je veden jako resitel | bez sloupce Resi, byl by tam porad on |
|
||||
| Nezarazene | kazdy | radky s rychlym prirazenim |
|
||||
| Vsechny tickety | kdo vidi i cizi tickety | sloupec Resi a filtr na sekce |
|
||||
|
||||
**Sloupec Resi je jen ve Vsech.** V Mych by ve vsech radcich stalo tyz jmeno
|
||||
a ve fronte je z definice prazdny. Presne o tom mluvil pozadavek: az v prehledu
|
||||
vsech ma smysl videt, kdo co ma u sebe.
|
||||
|
||||
Zalozka Vsechny se nenabizi tomu, kdo vidi jen svoje - byl by to tentyz seznam.
|
||||
Rika to `Access.seesOthers`, a `Access.visibleGroups` k tomu prida sekce, ktere
|
||||
smi filtrovat: kdo vidi celou firmu vsechny, vedouci jen ty, ktere vede.
|
||||
Nabidnout mu sekci, ze ktere stejne nic neuvidi, je jen matouci.
|
||||
|
||||
### Fronta umi prirazovat
|
||||
|
||||
Nova `QuickAssign`: u kazdeho ticketu ve fronte tlacitko **Vzit si** a dve
|
||||
roletky, komu a do ktere sekce. Prirazeni je ve fronte hlavni prace, ne jedna
|
||||
z mnoha veci na detailu - otevirat kvuli tomu kazdy ticket znamena u dvaceti
|
||||
ticketech ctyricet kliknuti navic.
|
||||
|
||||
Dve roletky, ne jedna: clovek a sekce jsou dve ruzna rozhodnuti. "Tohle je pro
|
||||
ucetni" jde rict bez toho, aby se resilo, kdo z nich ma dovolenou.
|
||||
|
||||
Kdo nesmi rozdavat praci ostatnim, vidi jen "Vzit si". Endpointy uz existovaly
|
||||
(`/claim`, `/assign`, `/group`), nova je jen cesta k nim.
|
||||
|
||||
### Vychozi zalozka podle adresy
|
||||
|
||||
Odkaz z widgetu nese `assignee`, takze `unassigned` otevre frontu a konkretni
|
||||
resitel nebo sekce pohled Vsechny. Bez toho by clovek prisel z dlazdice "fronta
|
||||
bez resitele" a koukal na svoje tickety.
|
||||
|
||||
Kdo neni veden jako resitel, nema zalozku Moje. Vychozi stav ji ale predpoklada,
|
||||
protoze prava dorazi az po prvnim vykresleni - proto pojistka, ktera po nacteni
|
||||
prav prepne na prvni dostupnou zalozku.
|
||||
|
||||
### Hledani s tim pocita
|
||||
|
||||
Zapsano do navrhu: hledani ma zapadnout do zalozky, ve ktere clovek stoji, ne ji
|
||||
obejit. Z Mych hleda ve svych, z Nezarazenych ve fronte, z Vsech v celem rozsahu
|
||||
stropu, a vysledek se vraci do te same zalozky. Volba "hledat vsude" dava smysl
|
||||
jen tam, kde zalozka Vsechny vubec je.
|
||||
|
||||
### Overeno na bezici instanci
|
||||
|
||||
| Kdo | Zalozka Vsechny | Sekce ve filtru |
|
||||
| ------------------------- | --------------- | --------------- |
|
||||
| spravce platformy | ano | vsechny |
|
||||
| Vomacka, vede Servicedesk | ano | jen Servicedesk |
|
||||
| Kriz, radovy clen | ne | zadne |
|
||||
|
||||
A cely pohyb ticketu frontou: admin preda TK-4819 do sekce, Kriz ho vidi ve
|
||||
fronte, vezme si ho a objevi se mu v Mych ticketech.
|
||||
|
||||
## 2026-09-02 - Nezarazene vidi kazdy, a dlazdice "Moje tickety"
|
||||
|
||||
Dve veci, ktere ze stropu viditelnosti vypadly.
|
||||
|
||||
@@ -102,6 +102,20 @@ export interface Access {
|
||||
nav: NavItem[];
|
||||
/** true = vidi napric firmami a smi platformni nastaveni. */
|
||||
platformAdmin: boolean;
|
||||
/**
|
||||
* Vidi i cizi tickety, ne jen svoje a frontu?
|
||||
*
|
||||
* Podle toho se nabizi zalozka "Vsechny tickety". Kdo vidi jen svoje,
|
||||
* nema co prepinat - zalozka by mu ukazala totez co ta prvni.
|
||||
*/
|
||||
seesOthers: boolean;
|
||||
/**
|
||||
* Sekce, na ktere clovek vidi. Filtr v zalozce "Vsechny tickety".
|
||||
*
|
||||
* Kdo vidi celou firmu, dostane vsechny. Vedouci jen ty, ktere vede -
|
||||
* nabidnout mu sekci, ze ktere stejne nic neuvidi, je jen matouci.
|
||||
*/
|
||||
visibleGroups: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +147,13 @@ export function accessFor(user: User, tenantId?: string | null): Access {
|
||||
const defaultTenantId = tenants[0]?.id ?? null;
|
||||
const activeTenant = tenantId ?? defaultTenantId;
|
||||
|
||||
const visibility = visibilityFor(user, activeTenant);
|
||||
const groups = activeTenant ? listGroups([activeTenant]) : [];
|
||||
const visibleGroups =
|
||||
visibility.kind === 'all'
|
||||
? groups
|
||||
: groups.filter((group) => visibility.groupIds.includes(group.id));
|
||||
|
||||
return {
|
||||
scopes,
|
||||
tenants: tenants.map((tenant) => ({ id: tenant.id, name: tenant.name })),
|
||||
@@ -145,6 +166,8 @@ export function accessFor(user: User, tenantId?: string | null): Access {
|
||||
permissions: [...permissionsOf(user, activeTenant)].sort(),
|
||||
nav: navFor(user, activeTenant),
|
||||
platformAdmin: user.platformAdmin,
|
||||
seesOthers: visibility.kind === 'all' || visibility.groupIds.length > 0,
|
||||
visibleGroups: visibleGroups.map((group) => ({ id: group.id, name: group.name })),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Rychle prirazeni ticketu z fronty.
|
||||
*
|
||||
* Fronta nezarazenych je jedine misto, kde je prirazeni **hlavni prace**, ne
|
||||
* jedna z mnoha veci na detailu. Otevirat kvuli tomu ticket a hledat v nem
|
||||
* roletku znamena u dvaceti ticketu ctyricet kliknuti navic.
|
||||
*
|
||||
* Dve roletky, ne jedna: clovek a sekce jsou dve ruzna rozhodnuti. "Tohle je
|
||||
* pro ucetni" jde rict bez toho, aby se resilo, kdo z nich ma dovolenou.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const selectClass =
|
||||
'rounded-lg border border-ink-600/70 bg-ink-900/70 px-2.5 py-1.5 text-xs text-white focus:border-brand-400/70 focus:outline-none disabled:opacity-50';
|
||||
|
||||
export function QuickAssign({
|
||||
ticketId,
|
||||
people,
|
||||
groups,
|
||||
canAssignOthers,
|
||||
meId,
|
||||
onDone,
|
||||
}: {
|
||||
ticketId: string;
|
||||
people: Array<{ id: string; name: string }>;
|
||||
groups: Array<{ id: string; name: string }>;
|
||||
/** false = clovek smi ticket vzit jen na sebe, ne ho rozdavat. */
|
||||
canAssignOthers: boolean;
|
||||
meId: string | null;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function send(path: string, body: unknown) {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await apiFetch(`/api/dashboard/tickets/${ticketId}${path}`, { method: 'POST', body });
|
||||
onDone();
|
||||
} catch (err) {
|
||||
console.error(`[fronta] ${path} selhalo:`, err);
|
||||
setError(err instanceof Error ? err.message : 'Nepodařilo se uložit.');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/*
|
||||
Vzit na sebe je nejcastejsi pohyb ve fronte, proto tlacitko a ne
|
||||
polozka v roletce. Ma ho i ten, kdo nesmi rozdavat praci ostatnim.
|
||||
*/}
|
||||
{meId && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => void send('/claim', {})}
|
||||
className={cn(
|
||||
'rounded-full bg-brand-400 px-3 py-1.5 text-xs font-semibold text-ink-950',
|
||||
'transition-colors hover:bg-brand-300 disabled:opacity-50',
|
||||
)}
|
||||
>
|
||||
Vzít si
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canAssignOthers && people.length > 0 && (
|
||||
<>
|
||||
<label htmlFor={`prirad-${ticketId}`} className="sr-only">
|
||||
Přiřadit řešiteli
|
||||
</label>
|
||||
<select
|
||||
id={`prirad-${ticketId}`}
|
||||
value=""
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
if (event.target.value) void send('/assign', { assigneeId: event.target.value });
|
||||
}}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="" className="bg-ink-850">
|
||||
Komu...
|
||||
</option>
|
||||
{people.map((person) => (
|
||||
<option key={person.id} value={person.id} className="bg-ink-850">
|
||||
{person.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<>
|
||||
<label htmlFor={`sekce-${ticketId}`} className="sr-only">
|
||||
Předat sekci
|
||||
</label>
|
||||
<select
|
||||
id={`sekce-${ticketId}`}
|
||||
value=""
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
if (event.target.value) void send('/group', { groupId: event.target.value });
|
||||
}}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="" className="bg-ink-850">
|
||||
Do sekce...
|
||||
</option>
|
||||
{groups.map((group) => (
|
||||
<option key={group.id} value={group.id} className="bg-ink-850">
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <span className="text-xs text-danger-400">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plus, Search } from 'lucide-react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TicketStatusBadge,
|
||||
} from '@/components/dashboard/StatusBadge';
|
||||
import { NewTicketDialog } from '@/components/dashboard/NewTicketDialog';
|
||||
import { QuickAssign } from '@/components/dashboard/QuickAssign';
|
||||
import { TicketWorkload } from '@/components/dashboard/TicketWorkload';
|
||||
import { TicketTable } from '@/components/dashboard/TicketTable';
|
||||
import { ViewSwitch } from '@/components/dashboard/ViewSwitch';
|
||||
@@ -20,6 +21,7 @@ import { useApiQuery } from '@/lib/useApiQuery';
|
||||
import { usePageMeta } from '@/lib/usePageMeta';
|
||||
import type {
|
||||
Access,
|
||||
PeopleResponse,
|
||||
TicketChannel,
|
||||
TicketListResponse,
|
||||
TicketScope,
|
||||
@@ -62,7 +64,23 @@ export default function Tickets() {
|
||||
groupId: params.get('groupId'),
|
||||
}).current;
|
||||
|
||||
const [scope, setScope] = useState<TicketScope | null>(initial.tenantId ? 'tenant' : null);
|
||||
/*
|
||||
* Vychozi zalozka podle adresy.
|
||||
*
|
||||
* Odkaz z widgetu nese `assignee`, takze `unassigned` znamena frontu
|
||||
* a konkretni resitel pohled na vsechny. Bez toho by clovek prisel
|
||||
* z dlazdice "fronta bez resitele" a koukal na svoje tickety.
|
||||
*/
|
||||
const initialTab: 'moje' | 'fronta' | 'vsechny' =
|
||||
initial.assignee === 'unassigned'
|
||||
? 'fronta'
|
||||
: initial.assignee !== null || initial.groupId !== null
|
||||
? 'vsechny'
|
||||
: 'moje';
|
||||
|
||||
const [scope, setScope] = useState<TicketScope | null>(
|
||||
initialTab === 'moje' ? 'mine' : initial.tenantId ? 'tenant' : null,
|
||||
);
|
||||
const [tenantId, setTenantId] = useState<string | null>(initial.tenantId);
|
||||
/** null = vsichni, 'unassigned' = fronta, jinak ID resitele. */
|
||||
const [assignee, setAssignee] = useState<string | null>(initial.assignee);
|
||||
@@ -78,7 +96,16 @@ export default function Tickets() {
|
||||
const [view, setView] = useState<'table' | 'grid'>('table');
|
||||
const [creating, setCreating] = useState(false);
|
||||
/** Jen moje. Server to resi pohledem `mine`, klient jen prepina. */
|
||||
const [mine, setMine] = useState(false);
|
||||
const [mine, setMine] = useState(initialTab === 'moje');
|
||||
|
||||
/*
|
||||
* Tri zalozky misto prepinacu.
|
||||
*
|
||||
* Puvodne to byly dva chipy vedle ostatnich filtru a splyvaly s nimi.
|
||||
* Jenze "co mam u sebe", "co jeste nikdo nema" a "co je ve firme" nejsou
|
||||
* filtry, jsou to tri ruzne prace - a kazda chce jinou tabulku.
|
||||
*/
|
||||
const [tab, setTab] = useState(initialTab);
|
||||
|
||||
// Filtrovani resi server, aby seznam a prehled nikdy neukazovaly jina cisla.
|
||||
const path = useMemo(() => {
|
||||
@@ -107,6 +134,10 @@ export default function Tickets() {
|
||||
|
||||
const tickets = useApiQuery<TicketListResponse>(path, { refetchOn: [...ticketEvents] });
|
||||
const workload = useApiQuery<Workload>(workloadPath, { refetchOn: [...ticketEvents] });
|
||||
// Do fronty: komu se da ticket predat. Nacita se jen pro zalozku Nezarazene.
|
||||
const people = useApiQuery<PeopleResponse>('/api/dashboard/people', {
|
||||
enabled: tab === 'fronta',
|
||||
});
|
||||
|
||||
// Hledani je jen dohledani v uz nactenem seznamu, proto na klientovi.
|
||||
const items = useMemo(() => {
|
||||
@@ -122,16 +153,54 @@ export default function Tickets() {
|
||||
);
|
||||
}, [tickets.data, query]);
|
||||
|
||||
/** Prepnuti zalozky. Jedno misto, kde se meni pohled i filtr na resitele. */
|
||||
function openTab(next: 'moje' | 'fronta' | 'vsechny') {
|
||||
setTab(next);
|
||||
setGroupId(null);
|
||||
if (next === 'moje') {
|
||||
setMine(true);
|
||||
setScope('mine');
|
||||
setAssignee(null);
|
||||
} else {
|
||||
setMine(false);
|
||||
setScope(scope === 'all' ? 'all' : 'tenant');
|
||||
setAssignee(next === 'fronta' ? 'unassigned' : null);
|
||||
}
|
||||
}
|
||||
|
||||
const data = tickets.data;
|
||||
const meId = data?.meId ?? null;
|
||||
const filtered = assignee !== null || status !== null || channel !== null;
|
||||
|
||||
const canCreate = access.data?.permissions.includes('ticket.create') === true;
|
||||
const canAssignOthers = access.data?.canAssignOthers === true;
|
||||
/** Sekce, na ktere clovek vidi. Filtr v zalozce Vsechny. */
|
||||
const visibleGroups = access.data?.visibleGroups ?? [];
|
||||
|
||||
const allowedScopes = access.data?.scopes ?? [];
|
||||
const tenants = access.data?.tenants ?? [];
|
||||
// Dokud server neodpovi, drzime se toho, co poslal u seznamu.
|
||||
const activeScope = scope ?? data?.scope ?? null;
|
||||
|
||||
const ticketTabs = ([
|
||||
{ key: 'moje', label: 'Moje tickety', show: allowedScopes.includes('mine') },
|
||||
{ key: 'fronta', label: 'Nezařazené', show: true },
|
||||
// Kdo vidi jen svoje, nema co prepinat: byl by to tentyz seznam.
|
||||
{ key: 'vsechny', label: 'Všechny tickety', show: access.data?.seesOthers === true },
|
||||
] as const).filter((item) => item.show);
|
||||
|
||||
/*
|
||||
* Kdo neni veden jako resitel, nema zalozku Moje. Vychozi stav ji ale
|
||||
* predpoklada, protoze prava dorazi az po prvnim vykresleni - bez teto
|
||||
* pojistky by koukal na prazdno a zadna zalozka by nesvitila.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!access.data) return;
|
||||
if (ticketTabs.some((item) => item.key === tab)) return;
|
||||
openTab(ticketTabs[0]?.key ?? 'fronta');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [access.data]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex flex-wrap items-end justify-between gap-4">
|
||||
@@ -165,6 +234,28 @@ export default function Tickets() {
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1fr_20rem]">
|
||||
<div className="space-y-4">
|
||||
{/*
|
||||
Zalozky, ne filtry. Kdo vidi jen svoje, nema co prepinat na
|
||||
"vsechny" - videl by tam totez a jen by ho to matlo.
|
||||
*/}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ticketTabs.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => openTab(item.key)}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
tab === item.key
|
||||
? 'bg-brand-500/15 text-brand-100'
|
||||
: 'text-white/50 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="glass rounded-card p-4 sm:p-5">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-white/35" />
|
||||
@@ -213,36 +304,29 @@ export default function Tickets() {
|
||||
</select>
|
||||
)}
|
||||
|
||||
{allowedScopes.includes('mine') && (
|
||||
<Chip
|
||||
active={mine}
|
||||
onClick={() => {
|
||||
const next = !mine;
|
||||
setMine(next);
|
||||
setScope(next ? 'mine' : activeScope === 'all' ? 'all' : 'tenant');
|
||||
if (next) setAssignee(null);
|
||||
}}
|
||||
title="Jen tickety, které mám u sebe."
|
||||
{/*
|
||||
Sekce jen v zalozce Vsechny. V Mych je vzdycky moje a ve
|
||||
fronte jeste zadna neni - ticket bez resitele casto nema
|
||||
ani skupinu, prave proto v te fronte lezi.
|
||||
*/}
|
||||
{tab === 'vsechny' && visibleGroups.length > 0 && (
|
||||
<select
|
||||
value={groupId ?? ''}
|
||||
onChange={(event) => setGroupId(event.target.value || null)}
|
||||
aria-label="Sekce"
|
||||
className="rounded-full border border-ink-600/70 bg-ink-850/70 px-3 py-1.5 text-xs text-white focus:border-brand-400/70 focus:outline-none"
|
||||
>
|
||||
Moje tickety
|
||||
</Chip>
|
||||
<option value="" className="bg-ink-850">
|
||||
Všechny sekce
|
||||
</option>
|
||||
{visibleGroups.map((group) => (
|
||||
<option key={group.id} value={group.id} className="bg-ink-850">
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
<Chip
|
||||
active={assignee === 'unassigned'}
|
||||
onClick={() => {
|
||||
setAssignee(assignee === 'unassigned' ? null : 'unassigned');
|
||||
// Fronta a moje se vylucuji, ticket ve fronte nema resitele.
|
||||
if (assignee !== 'unassigned' && mine) {
|
||||
setMine(false);
|
||||
setScope('tenant');
|
||||
}
|
||||
}}
|
||||
title="Tickety, které nemá nikdo u sebe."
|
||||
>
|
||||
Ve frontě
|
||||
</Chip>
|
||||
|
||||
{/*
|
||||
Filtry z odkazu. V UI k nim tlacitko neni, ale musi jit zrusit -
|
||||
jinak clovek prijde z widgetu a uz se z uzkeho vyberu nedostane.
|
||||
@@ -371,10 +455,56 @@ export default function Tickets() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : tab === 'fronta' ? (
|
||||
/*
|
||||
Fronta se kresli jinak nez seznam. Prirazeni je tady hlavni
|
||||
prace, ne jedna z mnoha veci na detailu - otevirat kvuli
|
||||
tomu kazdy ticket znamena u dvaceti tiketu ctyricet kliknuti
|
||||
navic.
|
||||
*/
|
||||
<ul className="divide-y divide-ink-600/40">
|
||||
{items.map((ticket) => (
|
||||
<li key={ticket.id} className="flex flex-wrap items-center gap-3 p-3 sm:p-4">
|
||||
<Link to={`/dashboard/tickety/${ticket.id}`} className="min-w-0 flex-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-xs text-white/35">{ticket.id}</span>
|
||||
<TicketStatusBadge status={ticket.status} closed={ticket.closed} />
|
||||
<TicketPriorityBadge priority={ticket.priority} />
|
||||
<span className="text-xs text-white/35">
|
||||
{formatRelative(ticket.updatedAt)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-sm text-white/85">
|
||||
{ticket.subject}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<QuickAssign
|
||||
ticketId={ticket.id}
|
||||
people={people.data?.items ?? []}
|
||||
groups={people.data?.groups ?? []}
|
||||
canAssignOthers={canAssignOthers}
|
||||
meId={meId}
|
||||
onDone={() => {
|
||||
tickets.reload();
|
||||
workload.reload();
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
/* Tatáz tabulka jako na detailu osoby, vcetne chovani na mobilu. */
|
||||
<div className="p-2 sm:p-3">
|
||||
<TicketTable tickets={items} meId={meId} />
|
||||
{/*
|
||||
Sloupec Resi dava smysl jen tam, kde jsou tickety vic lidi.
|
||||
V Mych by ve vsech radcich stalo moje jmeno.
|
||||
*/}
|
||||
<TicketTable
|
||||
tickets={items}
|
||||
meId={meId}
|
||||
hide={tab === 'vsechny' ? undefined : ['assignee']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DataState>
|
||||
|
||||
@@ -49,6 +49,10 @@ export interface Access {
|
||||
/** Zalozky ze serveru: prunik toho, co firma ma, a toho, na co ma pravo. */
|
||||
nav: NavEntry[];
|
||||
platformAdmin: boolean;
|
||||
/** true = vidi i cizi tickety. Podle toho se nabizi zalozka Vsechny. */
|
||||
seesOthers: boolean;
|
||||
/** Sekce, na ktere clovek vidi. Filtr v zalozce Vsechny. */
|
||||
visibleGroups: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
/** Resitel ticketu. Nemusi mit ucet v portalu, spojka je e-mail. */
|
||||
|
||||
Reference in New Issue
Block a user