feat(web): Employees + Roles pages with permissions matrix
EmployeesPage (/settings/employees): - Таблица: ФИО + должность, Роль, Email, Телефон, Учётка (есть/нет), Статус (Активен/Уволен). - Модалка добавления: ФИО + Position + Email + Phone + Role. Если выбрана роль «Кассир» — появляется блок «Кассы» с чекбоксами привязки к RetailPoint'ам (multi-select). - Чекбокс «Создать учётную запись» (по умолчанию ✓): сервер возвращает generatedPassword один раз, показываем в отдельной модалке с copy-кнопками логина и временного пароля. - Update/Delete как обычно. Снять Активен → серверная установка FiredAt. EmployeeRolesPage (/settings/employee-roles): - Таблица системных + кастомных ролей с счётчиком активных прав (N/21). Системные помечены бэйджем «Системная». - Модалка edit: имя, описание, матрица прав сгруппированная по 6 блокам (Каталог/Закупки/Продажи/Контрагенты/Отчёты/Настройки). Удаление кнопка только для кастомных. Меню «Настройки организации» дополнено пунктами «Сотрудники» (иконка UserCog) и «Роли» (иконка Shield). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c714ec265c
commit
f5cccb6f10
|
|
@ -13,6 +13,8 @@ import { ProductsPage } from '@/pages/ProductsPage'
|
|||
import { ProductEditPage } from '@/pages/ProductEditPage'
|
||||
import { MoySkladImportPage } from '@/pages/MoySkladImportPage'
|
||||
import { OrganizationSettingsPage } from '@/pages/OrganizationSettingsPage'
|
||||
import { EmployeesPage } from '@/pages/EmployeesPage'
|
||||
import { EmployeeRolesPage } from '@/pages/EmployeeRolesPage'
|
||||
import { StockPage } from '@/pages/StockPage'
|
||||
import { StockMovementsPage } from '@/pages/StockMovementsPage'
|
||||
import { SuppliesPage } from '@/pages/SuppliesPage'
|
||||
|
|
@ -60,6 +62,8 @@ export default function App() {
|
|||
<Route path="/sales/retail/:id" element={<RetailSaleEditPage />} />
|
||||
<Route path="/admin/import/moysklad" element={<MoySkladImportPage />} />
|
||||
<Route path="/settings/organization" element={<OrganizationSettingsPage />} />
|
||||
<Route path="/settings/employees" element={<EmployeesPage />} />
|
||||
<Route path="/settings/employee-roles" element={<EmployeeRolesPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { logout } from '@/lib/auth'
|
|||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
LayoutDashboard, Package, FolderTree, Ruler, Tag,
|
||||
Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download,
|
||||
Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, UserCog, Shield,
|
||||
Boxes, History, TruckIcon, ShoppingCart, Settings, Menu, X,
|
||||
} from 'lucide-react'
|
||||
import { Logo } from './Logo'
|
||||
|
|
@ -57,6 +57,8 @@ function buildNav(): NavSection[] {
|
|||
{ to: '/settings/organization', icon: Settings, label: 'Общие' },
|
||||
{ to: '/catalog/stores', icon: Warehouse, label: 'Склады' },
|
||||
{ to: '/catalog/retail-points', icon: StoreIcon, label: 'Кассы' },
|
||||
{ to: '/settings/employees', icon: UserCog, label: 'Сотрудники' },
|
||||
{ to: '/settings/employee-roles', icon: Shield, label: 'Роли' },
|
||||
]},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
198
src/food-market.web/src/pages/EmployeeRolesPage.tsx
Normal file
198
src/food-market.web/src/pages/EmployeeRolesPage.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { useState } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { ListPageShell } from '@/components/ListPageShell'
|
||||
import { DataTable } from '@/components/DataTable'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import { SearchBar } from '@/components/SearchBar'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { Field, TextInput, TextArea, Checkbox } from '@/components/Field'
|
||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||
|
||||
const URL = '/api/organization/employee-roles'
|
||||
|
||||
export interface RolePermissions {
|
||||
productsView: boolean; productsEdit: boolean; productsDelete: boolean
|
||||
productGroupsManage: boolean; priceTypesManage: boolean
|
||||
suppliesView: boolean; suppliesEdit: boolean; suppliesPost: boolean; suppliesDelete: boolean
|
||||
retailSalesOperate: boolean; retailSalesRefund: boolean
|
||||
counterpartiesView: boolean; counterpartiesEdit: boolean
|
||||
reportsView: boolean; stocksView: boolean
|
||||
orgSettingsManage: boolean; employeesManage: boolean; rolesManage: boolean
|
||||
storesManage: boolean; retailPointsManage: boolean
|
||||
}
|
||||
|
||||
export interface EmployeeRoleDto {
|
||||
id: string; name: string; description: string | null
|
||||
isSystem: boolean; sortOrder: number; permissions: RolePermissions
|
||||
}
|
||||
|
||||
interface Form {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
isSystem: boolean
|
||||
permissions: RolePermissions
|
||||
}
|
||||
|
||||
const blankPerms = (): RolePermissions => ({
|
||||
productsView: false, productsEdit: false, productsDelete: false,
|
||||
productGroupsManage: false, priceTypesManage: false,
|
||||
suppliesView: false, suppliesEdit: false, suppliesPost: false, suppliesDelete: false,
|
||||
retailSalesOperate: false, retailSalesRefund: false,
|
||||
counterpartiesView: false, counterpartiesEdit: false,
|
||||
reportsView: false, stocksView: false,
|
||||
orgSettingsManage: false, employeesManage: false, rolesManage: false,
|
||||
storesManage: false, retailPointsManage: false,
|
||||
})
|
||||
|
||||
const blankForm = (): Form => ({ name: '', description: '', isSystem: false, permissions: blankPerms() })
|
||||
|
||||
const PERM_GROUPS: { title: string; perms: { key: keyof RolePermissions; label: string }[] }[] = [
|
||||
{ title: 'Каталог', perms: [
|
||||
{ key: 'productsView', label: 'Просмотр товаров' },
|
||||
{ key: 'productsEdit', label: 'Редактирование товаров' },
|
||||
{ key: 'productsDelete', label: 'Удаление товаров' },
|
||||
{ key: 'productGroupsManage', label: 'Управление группами товаров' },
|
||||
{ key: 'priceTypesManage', label: 'Управление типами цен' },
|
||||
]},
|
||||
{ title: 'Закупки', perms: [
|
||||
{ key: 'suppliesView', label: 'Просмотр приёмок' },
|
||||
{ key: 'suppliesEdit', label: 'Редактирование приёмок' },
|
||||
{ key: 'suppliesPost', label: 'Проведение приёмок' },
|
||||
{ key: 'suppliesDelete', label: 'Удаление приёмок' },
|
||||
]},
|
||||
{ title: 'Продажи', perms: [
|
||||
{ key: 'retailSalesOperate', label: 'Работа на кассе' },
|
||||
{ key: 'retailSalesRefund', label: 'Возвраты' },
|
||||
]},
|
||||
{ title: 'Контрагенты', perms: [
|
||||
{ key: 'counterpartiesView', label: 'Просмотр' },
|
||||
{ key: 'counterpartiesEdit', label: 'Редактирование' },
|
||||
]},
|
||||
{ title: 'Отчёты и остатки', perms: [
|
||||
{ key: 'reportsView', label: 'Просмотр отчётов' },
|
||||
{ key: 'stocksView', label: 'Просмотр остатков' },
|
||||
]},
|
||||
{ title: 'Настройки организации', perms: [
|
||||
{ key: 'orgSettingsManage', label: 'Общие настройки' },
|
||||
{ key: 'employeesManage', label: 'Сотрудники' },
|
||||
{ key: 'rolesManage', label: 'Роли' },
|
||||
{ key: 'storesManage', label: 'Склады' },
|
||||
{ key: 'retailPointsManage', label: 'Кассы' },
|
||||
]},
|
||||
]
|
||||
|
||||
export function EmployeeRolesPage() {
|
||||
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<EmployeeRoleDto>(URL)
|
||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||
const [form, setForm] = useState<Form | null>(null)
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
const payload = { name: form.name, description: form.description || null, permissions: form.permissions }
|
||||
if (form.id) await update.mutateAsync({ id: form.id, input: payload })
|
||||
else await create.mutateAsync(payload)
|
||||
setForm(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListPageShell
|
||||
title="Роли сотрудников"
|
||||
description="Системные роли можно редактировать (галки прав), но не удалять. Кастомные — полный CRUD."
|
||||
actions={
|
||||
<>
|
||||
<SearchBar value={search} onChange={setSearch} />
|
||||
<Button onClick={() => setForm(blankForm())}>
|
||||
<Plus className="w-4 h-4" /> Добавить роль
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
footer={data && data.total > 0 && (
|
||||
<Pagination page={page} pageSize={data.pageSize} total={data.total} onPageChange={setPage} />
|
||||
)}
|
||||
>
|
||||
<DataTable
|
||||
rows={data?.items ?? []}
|
||||
isLoading={isLoading}
|
||||
rowKey={(r) => r.id}
|
||||
sortKey={sortKey}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={setSort}
|
||||
onRowClick={(r) => setForm({
|
||||
id: r.id, name: r.name, description: r.description ?? '',
|
||||
isSystem: r.isSystem, permissions: { ...blankPerms(), ...r.permissions },
|
||||
})}
|
||||
columns={[
|
||||
{ header: 'Название', cell: (r) => (
|
||||
<div>
|
||||
<div className="font-medium">{r.name}</div>
|
||||
{r.description && <div className="text-xs text-slate-400">{r.description}</div>}
|
||||
</div>
|
||||
)},
|
||||
{ header: 'Тип', width: '140px', cell: (r) => r.isSystem
|
||||
? <span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800">Системная</span>
|
||||
: <span className="text-xs text-slate-500">Кастомная</span> },
|
||||
{ header: 'Прав активно', width: '160px', className: 'text-right', cell: (r) => {
|
||||
const count = Object.values(r.permissions ?? {}).filter(Boolean).length
|
||||
return <span className="font-mono">{count} / {Object.keys(r.permissions ?? {}).length}</span>
|
||||
}},
|
||||
]}
|
||||
/>
|
||||
</ListPageShell>
|
||||
|
||||
<Modal
|
||||
open={!!form}
|
||||
onClose={() => setForm(null)}
|
||||
title={form?.id ? 'Редактировать роль' : 'Новая роль'}
|
||||
width="max-w-2xl"
|
||||
footer={
|
||||
<>
|
||||
{form?.id && !form.isSystem && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить роль?')) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null)
|
||||
}
|
||||
}}>
|
||||
<Trash2 className="w-4 h-4" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" onClick={() => setForm(null)}>Отмена</Button>
|
||||
<Button onClick={save} disabled={!form?.name}>Сохранить</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{form && (
|
||||
<div className="space-y-4">
|
||||
<Field label="Название *">
|
||||
<TextInput value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Описание">
|
||||
<TextArea rows={2} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||
</Field>
|
||||
<div className="border-t border-slate-200 dark:border-slate-700 pt-3 space-y-4">
|
||||
<h3 className="text-sm font-semibold">Права</h3>
|
||||
{PERM_GROUPS.map((g) => (
|
||||
<div key={g.title}>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-2">{g.title}</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
|
||||
{g.perms.map((p) => (
|
||||
<Checkbox
|
||||
key={p.key}
|
||||
label={p.label}
|
||||
checked={form.permissions[p.key]}
|
||||
onChange={(v) => setForm({ ...form, permissions: { ...form.permissions, [p.key]: v } })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
293
src/food-market.web/src/pages/EmployeesPage.tsx
Normal file
293
src/food-market.web/src/pages/EmployeesPage.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Plus, Trash2, Copy } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { ListPageShell } from '@/components/ListPageShell'
|
||||
import { DataTable } from '@/components/DataTable'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import { SearchBar } from '@/components/SearchBar'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||
import type { PagedResult, RetailPoint } from '@/lib/types'
|
||||
import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage'
|
||||
|
||||
const URL = '/api/organization/employees'
|
||||
|
||||
interface EmployeeDto {
|
||||
id: string
|
||||
userId: string | null
|
||||
lastName: string
|
||||
firstName: string
|
||||
middleName: string | null
|
||||
position: string | null
|
||||
email: string | null
|
||||
phone: string | null
|
||||
roleId: string
|
||||
roleName: string
|
||||
isActive: boolean
|
||||
firedAt: string | null
|
||||
retailPointIds: string[]
|
||||
}
|
||||
|
||||
interface Form {
|
||||
id?: string
|
||||
lastName: string
|
||||
firstName: string
|
||||
middleName: string
|
||||
position: string
|
||||
email: string
|
||||
phone: string
|
||||
roleId: string
|
||||
isActive: boolean
|
||||
retailPointIds: string[]
|
||||
createAccount: boolean
|
||||
}
|
||||
|
||||
const blankForm = (): Form => ({
|
||||
lastName: '', firstName: '', middleName: '', position: '',
|
||||
email: '', phone: '',
|
||||
roleId: '', isActive: true,
|
||||
retailPointIds: [],
|
||||
createAccount: true,
|
||||
})
|
||||
|
||||
export function EmployeesPage() {
|
||||
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<EmployeeDto>(URL)
|
||||
const { update, remove } = useCatalogMutations(URL, URL)
|
||||
const [form, setForm] = useState<Form | null>(null)
|
||||
// Сгенерированный пароль возвращается с сервера один раз — показываем
|
||||
// в отдельной модалке, чтобы админ передал сотруднику.
|
||||
const [createdAccount, setCreatedAccount] = useState<{ email: string; password: string } | null>(null)
|
||||
|
||||
const roles = useQuery({
|
||||
queryKey: ['employee-roles-lookup'],
|
||||
queryFn: async () => (await api.get<PagedResult<EmployeeRoleDto>>('/api/organization/employee-roles?pageSize=200')).data.items,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const retailPoints = useQuery({
|
||||
queryKey: ['retail-points-lookup'],
|
||||
queryFn: async () => (await api.get<PagedResult<RetailPoint>>('/api/catalog/retail-points?pageSize=200')).data.items,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
// Дефолт роли для нового сотрудника — первая по сортировке (Менеджер
|
||||
// если есть, иначе Кассир — селект всё равно покажет полный список).
|
||||
useEffect(() => {
|
||||
if (form && !form.roleId && roles.data?.length) {
|
||||
const def = roles.data.find((r) => r.name === 'Менеджер') ?? roles.data[0]
|
||||
setForm({ ...form, roleId: def.id })
|
||||
}
|
||||
}, [form, roles.data])
|
||||
|
||||
const selectedRole = roles.data?.find((r) => r.id === form?.roleId)
|
||||
const isCashier = selectedRole?.name === 'Кассир'
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
const payload = {
|
||||
lastName: form.lastName, firstName: form.firstName, middleName: form.middleName || null,
|
||||
position: form.position || null, email: form.email || null, phone: form.phone || null,
|
||||
roleId: form.roleId, isActive: form.isActive,
|
||||
retailPointIds: form.retailPointIds,
|
||||
createAccount: !form.id && form.createAccount,
|
||||
}
|
||||
if (form.id) {
|
||||
await update.mutateAsync({ id: form.id, input: payload })
|
||||
setForm(null)
|
||||
} else {
|
||||
const res = await api.post<{ employee: EmployeeDto; generatedPassword: string | null }>(URL, payload)
|
||||
setForm(null)
|
||||
// Если сервер вернул password — показываем модалку one-shot.
|
||||
if (res.data.generatedPassword && res.data.employee.email) {
|
||||
setCreatedAccount({ email: res.data.employee.email, password: res.data.generatedPassword })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRP = (id: string) => {
|
||||
if (!form) return
|
||||
const has = form.retailPointIds.includes(id)
|
||||
setForm({
|
||||
...form,
|
||||
retailPointIds: has ? form.retailPointIds.filter((x) => x !== id) : [...form.retailPointIds, id],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListPageShell
|
||||
title="Сотрудники"
|
||||
description="Учётные записи и сотрудники без логина. Привязка к ролям, для кассиров — к конкретным кассам."
|
||||
actions={
|
||||
<>
|
||||
<SearchBar value={search} onChange={setSearch} />
|
||||
<Button onClick={() => setForm(blankForm())}>
|
||||
<Plus className="w-4 h-4" /> Добавить сотрудника
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
footer={data && data.total > 0 && (
|
||||
<Pagination page={page} pageSize={data.pageSize} total={data.total} onPageChange={setPage} />
|
||||
)}
|
||||
>
|
||||
<DataTable
|
||||
rows={data?.items ?? []}
|
||||
isLoading={isLoading}
|
||||
rowKey={(r) => r.id}
|
||||
sortKey={sortKey}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={setSort}
|
||||
onRowClick={(r) => setForm({
|
||||
id: r.id, lastName: r.lastName, firstName: r.firstName, middleName: r.middleName ?? '',
|
||||
position: r.position ?? '', email: r.email ?? '', phone: r.phone ?? '',
|
||||
roleId: r.roleId, isActive: r.isActive, retailPointIds: r.retailPointIds,
|
||||
createAccount: false,
|
||||
})}
|
||||
columns={[
|
||||
{ header: 'ФИО', cell: (r) => (
|
||||
<div>
|
||||
<div className="font-medium">{r.lastName} {r.firstName} {r.middleName ?? ''}</div>
|
||||
{r.position && <div className="text-xs text-slate-400">{r.position}</div>}
|
||||
</div>
|
||||
)},
|
||||
{ header: 'Роль', width: '160px', cell: (r) => r.roleName },
|
||||
{ header: 'Email', width: '220px', cell: (r) => r.email ?? '—' },
|
||||
{ header: 'Телефон', width: '150px', cell: (r) => r.phone ?? '—' },
|
||||
{ header: 'Учётка', width: '110px', cell: (r) => r.userId
|
||||
? <span className="text-xs text-emerald-600">есть</span>
|
||||
: <span className="text-xs text-slate-400">нет</span> },
|
||||
{ header: 'Статус', width: '110px', cell: (r) => r.isActive
|
||||
? <span className="text-xs text-emerald-600">Активен</span>
|
||||
: <span className="text-xs text-slate-400">Уволен</span> },
|
||||
]}
|
||||
/>
|
||||
</ListPageShell>
|
||||
|
||||
<Modal
|
||||
open={!!form}
|
||||
onClose={() => setForm(null)}
|
||||
title={form?.id ? 'Редактировать сотрудника' : 'Новый сотрудник'}
|
||||
width="max-w-xl"
|
||||
footer={
|
||||
<>
|
||||
{form?.id && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить сотрудника?')) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null)
|
||||
}
|
||||
}}>
|
||||
<Trash2 className="w-4 h-4" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" onClick={() => setForm(null)}>Отмена</Button>
|
||||
<Button onClick={save} disabled={!form?.lastName || !form.firstName || !form.roleId}>Сохранить</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{form && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Фамилия *">
|
||||
<TextInput value={form.lastName} onChange={(e) => setForm({ ...form, lastName: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Имя *">
|
||||
<TextInput value={form.firstName} onChange={(e) => setForm({ ...form, firstName: e.target.value })} />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Отчество">
|
||||
<TextInput value={form.middleName} onChange={(e) => setForm({ ...form, middleName: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Должность">
|
||||
<TextInput value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="Кассир, кладовщик…" />
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Email">
|
||||
<TextInput type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="Телефон">
|
||||
<TextInput value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="Роль *">
|
||||
<Select value={form.roleId} onChange={(e) => setForm({ ...form, roleId: e.target.value })}>
|
||||
<option value="">—</option>
|
||||
{roles.data?.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||
</Select>
|
||||
</Field>
|
||||
{isCashier && (
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1.5">Кассы</div>
|
||||
<div className="border border-slate-200 dark:border-slate-700 rounded-md p-2 max-h-40 overflow-auto space-y-1">
|
||||
{retailPoints.data?.length === 0 && (
|
||||
<div className="text-xs text-slate-400">Нет касс. Добавь в Настройках.</div>
|
||||
)}
|
||||
{retailPoints.data?.map((rp) => (
|
||||
<Checkbox
|
||||
key={rp.id}
|
||||
label={rp.name}
|
||||
checked={form.retailPointIds.includes(rp.id)}
|
||||
onChange={() => toggleRP(rp.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">Если ничего не выбрано — кассир работает на всех кассах.</p>
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
label="Активен"
|
||||
checked={form.isActive}
|
||||
onChange={(v) => setForm({ ...form, isActive: v })}
|
||||
/>
|
||||
{!form.id && (
|
||||
<Checkbox
|
||||
label="Создать учётную запись (выдать логин)"
|
||||
checked={form.createAccount}
|
||||
onChange={(v) => setForm({ ...form, createAccount: v })}
|
||||
/>
|
||||
)}
|
||||
{!form.id && form.createAccount && !form.email && (
|
||||
<p className="text-xs text-amber-600">Для создания учётки укажи email — он будет логином.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={!!createdAccount}
|
||||
onClose={() => setCreatedAccount(null)}
|
||||
title="Учётная запись создана"
|
||||
footer={<Button onClick={() => setCreatedAccount(null)}>Готово</Button>}
|
||||
>
|
||||
{createdAccount && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Передайте сотруднику данные для входа. Это окно показывается один раз —
|
||||
после закрытия пароль восстановить нельзя, придётся сбросить.
|
||||
</p>
|
||||
<Field label="Логин (email)">
|
||||
<div className="flex gap-2">
|
||||
<TextInput value={createdAccount.email} readOnly />
|
||||
<Button variant="secondary" size="sm" onClick={() => navigator.clipboard?.writeText(createdAccount.email)}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Временный пароль">
|
||||
<div className="flex gap-2">
|
||||
<TextInput value={createdAccount.password} readOnly className="font-mono" />
|
||||
<Button variant="secondary" size="sm" onClick={() => navigator.clipboard?.writeText(createdAccount.password)}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue