Resitel je clenstvi uctu, firma z ARES, prepinani jazyku schovane
Resitel uz neni vlastni zaznam spojeny s uctem pres e-mail: je to clenstvi uctu ve firme a jeho ID je ID uctu. Popisek, kapacita, externi ID a zapnuti visi na clenstvi, takze clovek ve dvou firmach je v kazde jinak a spravce firmy ho vypne jen u sebe. Odebrani z firmy odebere jen clenstvi. Stara data se pri startu jednou prevedou (migratePeople.ts), vcetne odkazu v ticketech, skupinach, automatizacich, akcich a widgetech. Sprava lidi v zalozce Lide zaklada ucty, pozvanka uz nema volbu resitele. Zalozeni firmy z registru ARES: hledani podle IC nebo nazvu, dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem IC-poradi@placeholder.cz. Vychozi rozlozeni dashboardu bez resitele neobsahuje list.myTickets. Prepinani jazyku je docasne schovane (MULTILANG_ENABLED), web je cesky. Dokumentace aktualizovana. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6eed909a0d
commit
bc6508e1f3
@@ -131,7 +131,7 @@ export function AresTenantDialog({
|
||||
ico: company.ico,
|
||||
name: name.trim() || undefined,
|
||||
note: note.trim() || undefined,
|
||||
persons: selected.map((row) => ({ name: row.name.trim(), email: row.email.trim() })),
|
||||
persons: selected.map((row) => ({ name: row.name.trim(), email: row.email.trim(), roles: row.roles })),
|
||||
},
|
||||
});
|
||||
setCreated(result);
|
||||
@@ -161,7 +161,9 @@ export function AresTenantDialog({
|
||||
</p>
|
||||
{created.users.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-white/60">Založené účty, všechny jako správce firmy:</p>
|
||||
<p className="text-sm text-white/60">
|
||||
Založené účty, všechny jako správce firmy. Najdete je v záložce Firma:
|
||||
</p>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{created.users.map((user) => (
|
||||
<li key={user.id} className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -265,7 +265,7 @@ export function GroupPanel() {
|
||||
|
||||
{persons.length === 0 && (
|
||||
<p className="mt-4 text-sm text-white/40">
|
||||
Ve firmě zatím nikdo není. Nejdřív přidejte řešitele.
|
||||
Ve firmě zatím nikdo není. Nejdřív přidejte lidi v záložce Řešitelé nebo je pozvěte.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,6 @@ interface InviteRow {
|
||||
email: string;
|
||||
note: string;
|
||||
roleIds: string[];
|
||||
asPerson: boolean;
|
||||
expiresAt: string;
|
||||
usedAt: string | null;
|
||||
createdByEmail: string;
|
||||
@@ -47,7 +46,6 @@ export function InvitePanel() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [roleIds, setRoleIds] = useState<string[]>([]);
|
||||
const [asPerson, setAsPerson] = useState(true);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const roles = invites.data?.roles ?? [];
|
||||
@@ -56,7 +54,7 @@ export function InvitePanel() {
|
||||
const creation = useSubmit(async () => {
|
||||
await apiFetch('/api/dashboard/invites', {
|
||||
method: 'POST',
|
||||
body: { email, note, roleIds, asPerson },
|
||||
body: { email, note, roleIds },
|
||||
});
|
||||
setEmail('');
|
||||
setNote('');
|
||||
@@ -141,22 +139,7 @@ export function InvitePanel() {
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label className="mt-4 flex items-start gap-2 text-sm text-white/70">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={asPerson}
|
||||
onChange={(event) => setAsPerson(event.target.checked)}
|
||||
className="mt-0.5 size-4 accent-brand-400"
|
||||
/>
|
||||
<span>
|
||||
Založit i jako řešitele
|
||||
<span className="mt-0.5 block text-xs text-white/35">
|
||||
Řešitel je ten, komu jde přehodit ticket. Účetní může mít přístup do portálu,
|
||||
aniž by kdy ticket řešila.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Kazdy clen firmy je zaroven resitel, zadna volba "zalozit i jako resitele" uz neni. */}
|
||||
{error && <p className="mt-3 text-sm text-danger-400">{error}</p>}
|
||||
|
||||
<Button
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Logo } from '@/components/layout/Logo';
|
||||
import { ButtonLink } from '@/components/ui/Button';
|
||||
import { Container } from '@/components/ui/Container';
|
||||
import { mainNav } from '@/data/navigation';
|
||||
import { useT } from '@/i18n';
|
||||
import { MULTILANG_ENABLED, useT } from '@/i18n';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export function Navbar() {
|
||||
@@ -63,7 +63,7 @@ export function Navbar() {
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<LanguageSwitch />
|
||||
{MULTILANG_ENABLED && <LanguageSwitch />}
|
||||
{user ? (
|
||||
<ButtonLink to="/dashboard" size="sm">
|
||||
<LayoutDashboard className="size-4" />
|
||||
@@ -111,7 +111,7 @@ export function Navbar() {
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
<LanguageSwitch variant="full" className="mt-3 px-1" />
|
||||
{MULTILANG_ENABLED && <LanguageSwitch variant="full" className="mt-3 px-1" />}
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
{user ? (
|
||||
|
||||
+13
-1
@@ -27,6 +27,13 @@ export const locales: Array<{ code: Locale; label: string; short: string }> = [
|
||||
|
||||
const STORAGE_KEY = 'automia.locale';
|
||||
|
||||
/**
|
||||
* Jeden prepinac pro cely web. `false` = prepinac jazyka se nekresli
|
||||
* a vzdy se pouzije cestina. Az bude preklad hotovy a znacka rozhodnuta,
|
||||
* staci prepnout tady.
|
||||
*/
|
||||
export const MULTILANG_ENABLED = false;
|
||||
|
||||
const dictionaries: Record<Locale, Partial<Record<MessageKey, string>>> = { cs, en };
|
||||
|
||||
/**
|
||||
@@ -36,6 +43,11 @@ const dictionaries: Record<Locale, Partial<Record<MessageKey, string>>> = { cs,
|
||||
* az na druhem miste - kdo si jednou prepnul, mysli to vazne.
|
||||
*/
|
||||
function detect(): Locale {
|
||||
// Prepinani jazyku je docasne schovane (rozhodnuti 2026-09-09): web je jen
|
||||
// cesky, dokud se nerozhodne o znacce. Slovniky zustavaji, at se preklad
|
||||
// neztrati; ulozena volba `en` se ignoruje, jinak by nekdo zustal v anglictine
|
||||
// bez moznosti prepnout zpet.
|
||||
if (!MULTILANG_ENABLED) return 'cs';
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'cs' || stored === 'en') return stored;
|
||||
@@ -57,7 +69,7 @@ export function getLocale(): Locale {
|
||||
}
|
||||
|
||||
export function setLocale(locale: Locale): void {
|
||||
if (locale === active) return;
|
||||
if (!MULTILANG_ENABLED || locale === active) return;
|
||||
active = locale;
|
||||
|
||||
// Kvuli cteckam a prohlizecum, ktere podle toho nabizeji preklad stranky.
|
||||
|
||||
@@ -226,59 +226,7 @@ export default function People() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'resitele' && (
|
||||
<EntityAdmin
|
||||
title="Řešitelé"
|
||||
description="Lidé, na které jde přehodit ticket. Nemusí mít přístup do portálu, spojka je e-mail."
|
||||
endpoint="/api/dashboard/settings/people"
|
||||
listEndpoint="/api/dashboard/settings/people-overview"
|
||||
onChanged={people.reload}
|
||||
columns={[
|
||||
{ label: 'Jméno', render: (person: NamedRecord) => String(person.name ?? '') },
|
||||
{ label: 'E-mail', render: (person: NamedRecord) => String(person.email ?? '') },
|
||||
{ label: 'Zaměření', render: (person: NamedRecord) => String(person.role ?? '') },
|
||||
{
|
||||
label: 'Kapacita',
|
||||
narrow: true,
|
||||
render: (person: NamedRecord) => String(person.capacity ?? ''),
|
||||
},
|
||||
{
|
||||
label: 'Stav',
|
||||
narrow: true,
|
||||
render: (person: NamedRecord) =>
|
||||
person.enabled === false ? (
|
||||
<Badge tone="warn">vypnutý</Badge>
|
||||
) : (
|
||||
<Badge tone="ok">aktivní</Badge>
|
||||
),
|
||||
},
|
||||
]}
|
||||
fields={[
|
||||
{ name: 'name', label: 'Jméno', kind: 'text', required: true },
|
||||
{ name: 'email', label: 'E-mail', kind: 'text', required: true },
|
||||
{
|
||||
name: 'role',
|
||||
label: 'Zaměření',
|
||||
kind: 'text',
|
||||
hint: 'Jen popisek, nic nerozhoduje.',
|
||||
},
|
||||
{
|
||||
name: 'capacity',
|
||||
label: 'Kapacita',
|
||||
kind: 'number',
|
||||
initial: 8,
|
||||
hint: 'Kolik nevyřízených ticketů je pro něj zdravá zátěž. Není to limit.',
|
||||
},
|
||||
{
|
||||
name: 'externalIds',
|
||||
label: 'ID z cizích aplikací',
|
||||
kind: 'tags',
|
||||
hint: 'Oddělená čárkou. Podle nich umí automatizace předat ticket právě jemu, například voicebotId.',
|
||||
},
|
||||
{ name: 'enabled', label: 'Aktivní', kind: 'checkbox', initial: true },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{tab === 'resitele' && <PeopleAdmin onChanged={people.reload} />}
|
||||
|
||||
{tab === 'skupiny' && <GroupPanel />}
|
||||
|
||||
@@ -291,6 +239,140 @@ export default function People() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Role firmy, jak je vraci `/settings/roles-available`. Odkaz v clenstvi smi byt ID i klic. */
|
||||
interface RoleOption {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprava resitelu.
|
||||
*
|
||||
* Resitel neni vlastni zaznam, je to clen firmy: ID resitele je ID uctu.
|
||||
* Zalozeni tady proto zaklada ucet (nebo pridava clenstvi uz existujicimu)
|
||||
* a smazani jen rusi clenstvi. Driv byl resitel zvlastni zaznam spojeny
|
||||
* s uctem pres e-mail a zmena e-mailu tu vazbu rozbila.
|
||||
*
|
||||
* Vlastni komponenta, aby se role nacitaly az pri otevreni zalozky.
|
||||
*/
|
||||
function PeopleAdmin({ onChanged }: { onChanged: () => void }) {
|
||||
const roles = useApiQuery<{ items: RoleOption[] }>('/api/dashboard/settings/roles-available');
|
||||
const roleItems = useMemo(() => roles.data?.items ?? [], [roles.data]);
|
||||
|
||||
const roleOptions = useMemo(
|
||||
() => roleItems.map((role) => ({ value: role.id, label: role.name })),
|
||||
[roleItems],
|
||||
);
|
||||
// Vychozi role noveho cloveka: agent. Bez role by nemel v portalu co delat.
|
||||
const agentRoleId = roleItems.find((role) => role.key === 'agent')?.id;
|
||||
|
||||
/** Jmena roli podle odkazu v clenstvi (ID role nebo klic systemove role). */
|
||||
function roleNames(ids: unknown): string {
|
||||
if (!Array.isArray(ids)) return '-';
|
||||
return (ids as string[])
|
||||
.map((id) => roleItems.find((role) => role.id === id || role.key === id)?.name ?? id)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
return (
|
||||
<EntityAdmin
|
||||
title="Řešitelé"
|
||||
description="Lidé ve firmě. Každý člen má účet do portálu a může mít u sebe tickety. Smazání jen odebere členství ve firmě, účet zůstává."
|
||||
endpoint="/api/dashboard/settings/people"
|
||||
listEndpoint="/api/dashboard/settings/people-overview"
|
||||
refetchOn={['person.created', 'person.updated', 'person.deleted']}
|
||||
onChanged={onChanged}
|
||||
columns={[
|
||||
{ label: 'Jméno', render: (person: NamedRecord) => String(person.name ?? '') },
|
||||
{ label: 'E-mail', render: (person: NamedRecord) => String(person.email ?? '') },
|
||||
{ label: 'Zaměření', render: (person: NamedRecord) => String(person.role ?? '') },
|
||||
{
|
||||
label: 'Kapacita',
|
||||
narrow: true,
|
||||
render: (person: NamedRecord) => String(person.capacity ?? ''),
|
||||
},
|
||||
{ label: 'Role', render: (person: NamedRecord) => roleNames(person.roleIds) },
|
||||
{
|
||||
label: 'Stav',
|
||||
narrow: true,
|
||||
render: (person: NamedRecord) =>
|
||||
person.enabled === false ? (
|
||||
<Badge tone="warn">vypnutý</Badge>
|
||||
) : (
|
||||
<Badge tone="ok">aktivní</Badge>
|
||||
),
|
||||
},
|
||||
]}
|
||||
fields={[
|
||||
{ name: 'name', label: 'Jméno', kind: 'text', required: true },
|
||||
{ name: 'email', label: 'E-mail', kind: 'text', required: true },
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Heslo',
|
||||
kind: 'text',
|
||||
createOnly: true,
|
||||
hint: 'Prázdné = náhodné heslo, člověk si ho nastaví přes pozvánku nebo mu ho změní správce.',
|
||||
},
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Role ve firmě',
|
||||
kind: 'multiselect',
|
||||
options: roleOptions,
|
||||
initial: agentRoleId ? [agentRoleId] : [],
|
||||
// Bez roli v zaznamu by prazdny vyber pri uprave role tise smazal.
|
||||
visible: (person) => person === null || Array.isArray(person.roleIds),
|
||||
hint: 'Co smí v portálu dělat. Nový člověk je agent.',
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
label: 'Zaměření',
|
||||
kind: 'text',
|
||||
hint: 'Jen popisek, nic nerozhoduje.',
|
||||
},
|
||||
{
|
||||
name: 'capacity',
|
||||
label: 'Kapacita',
|
||||
kind: 'number',
|
||||
initial: 8,
|
||||
hint: 'Kolik nevyřízených ticketů je pro něj zdravá zátěž. Není to limit.',
|
||||
},
|
||||
{
|
||||
name: 'externalIds',
|
||||
label: 'ID z cizích aplikací',
|
||||
kind: 'tags',
|
||||
hint: 'Oddělená čárkou. Podle nich umí automatizace předat ticket právě jemu, například voicebotId.',
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
label: 'Aktivní v této firmě',
|
||||
kind: 'checkbox',
|
||||
initial: true,
|
||||
hint: 'Vypnutý se nenabízí k přiřazení, staré tickety mu zůstanou. Účet v jiných firmách to neovlivní.',
|
||||
},
|
||||
]}
|
||||
toBody={(values, entity) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
role: values.role,
|
||||
capacity: values.capacity,
|
||||
externalIds: values.externalIds,
|
||||
enabled: values.enabled !== false,
|
||||
};
|
||||
// Role se posilaji jen kdyz je bylo videt: pri zalozeni vzdy, pri uprave
|
||||
// jen kdyz je server v zaznamu vratil.
|
||||
if (!entity || Array.isArray(entity.roleIds)) body.roleIds = values.roleIds;
|
||||
// Prazdne heslo se neposila, server si vygeneruje nahodne.
|
||||
if (!entity && typeof values.password === 'string' && values.password !== '') {
|
||||
body.password = values.password;
|
||||
}
|
||||
return body;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({
|
||||
label,
|
||||
value,
|
||||
|
||||
@@ -200,7 +200,7 @@ export default function Tickets() {
|
||||
] as const).filter((item) => item.show);
|
||||
|
||||
/*
|
||||
* Kdo neni veden jako resitel, nema zalozku Moje. Vychozi stav ji ale
|
||||
* Kdo nesmi videt svoje tickety (chybi rozsah `mine`), 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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user