Переключатели в ячейках, счётчик выданных доступов в заголовке колонки, заблокированные ячейки администраторов, пометки «Не сохранено». Исправлена вёрстка рейки вкладок профиля. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
83 lines
3.4 KiB
TypeScript
83 lines
3.4 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { LockIcon } from 'lucide-react'
|
|
import { APPS, type AdminUser, type AppId } from '@authportal/shared'
|
|
import { Switch } from '@authportal/ui/components/switch'
|
|
import { PendingChip, SignalAlert } from './bits'
|
|
import { APP_SHORT } from './mock'
|
|
import type { RowMark } from './preview-store'
|
|
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid'
|
|
|
|
/**
|
|
* Матрица доступа (solution-users-3): пользователи в строках, приложения в колонках,
|
|
* переключатели в ячейках. Администраторам доступ выдан ко всем приложениям — их ячейки заблокированы.
|
|
*/
|
|
export function AccessMatrix({
|
|
users,
|
|
marks,
|
|
onToggle,
|
|
}: {
|
|
users: AdminUser[]
|
|
marks: Record<string, RowMark>
|
|
onToggle: (user: AdminUser, appId: AppId, on: boolean) => void
|
|
}) {
|
|
const columns = useMemo<GridColumn<AdminUser>[]>(() => {
|
|
const granted = (id: AppId) => users.filter((u) => u.is_admin || u.apps.includes(id)).length
|
|
return [
|
|
{
|
|
id: 'user',
|
|
title: 'Пользователь',
|
|
size: 260,
|
|
sortValue: (u) => u.email,
|
|
cell: (u) => (
|
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
<TwoLineCell dot={u.disabled ? 'mu' : 'ok'} title={u.email} sub={u.is_admin ? 'Админ · все приложения' : `${u.apps.length} из ${APPS.length}`} />
|
|
{marks[u.id] ? <PendingChip mark={marks[u.id]!} /> : null}
|
|
</div>
|
|
),
|
|
},
|
|
...APPS.map<GridColumn<AdminUser>>((a) => ({
|
|
id: a.id,
|
|
title: `${APP_SHORT[a.id]} · ${granted(a.id)}`,
|
|
size: 128,
|
|
cell: (u) =>
|
|
u.is_admin ? (
|
|
<span className="text-muted-foreground flex items-center gap-1.5 text-xs" title="Администратор: доступ ко всем приложениям">
|
|
<Switch size="sm" checked disabled aria-label={`${u.email}: ${a.title} (заблокировано)`} />
|
|
<LockIcon className="size-3" aria-hidden />
|
|
</span>
|
|
) : (
|
|
<Switch
|
|
size="sm"
|
|
checked={u.apps.includes(a.id)}
|
|
onCheckedChange={(on) => onToggle(u, a.id, on)}
|
|
aria-label={`${u.email}: ${a.title}`}
|
|
/>
|
|
),
|
|
})),
|
|
]
|
|
}, [users, marks, onToggle])
|
|
|
|
return (
|
|
<div className="flex min-w-0 flex-col gap-3">
|
|
<SignalAlert tone="in" title="Администраторы видят все приложения">
|
|
Их ячейки заблокированы. Тонкую настройку разделов откройте в правах пользователя.
|
|
</SignalAlert>
|
|
<PreviewSectionGrid
|
|
title="Матрица доступа"
|
|
description="В заголовке колонки — сколько пользователей имеют доступ к приложению"
|
|
rows={users}
|
|
columns={columns}
|
|
getRowId={(u) => u.id}
|
|
rowStatus={(u) => marks[u.id]}
|
|
searchText={(u) => u.email}
|
|
searchPlaceholder="Email"
|
|
views={[
|
|
{ label: 'Все', match: () => true },
|
|
{ label: 'Пользователи', match: (u) => !u.is_admin },
|
|
{ label: 'Админы', match: (u) => u.is_admin },
|
|
]}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|