// UsersPage — left list of users + center profile/role editor // Data model derives from ui/src/pages/UsersPage.tsx. const ROLE_TONE = { admin: { bg: '#ede9fe', fg: '#5b21b6' }, operator: { bg: '#dbeafe', fg: '#1d4ed8' }, viewer: { bg: '#e2e8f0', fg: '#475569' }, }; const USER_STATUS_TONE = { pending: { bg: '#fef9c3', fg: '#854d0e', label: '承認待ち' }, active: { bg: '#dcfce7', fg: '#166534', label: 'アクティブ' }, disabled: { bg: '#e2e8f0', fg: '#475569', label: '無効' }, }; function UserAvatar({ name, size = 32 }) { const initial = (name || '?').charAt(0).toUpperCase(); // simple deterministic hue from name let hue = 0; for (const c of (name || '')) hue = (hue * 31 + c.charCodeAt(0)) % 360; return (
{initial}
); } function UserListItem({ user, active, onClick }) { return ( ); } function UserListPane({ users, activeId, onSelect, filter, setFilter, search, setSearch, onOpenInvite }) { const filtered = users.filter(u => { if (filter !== 'all' && u.role !== filter && u.status !== filter) return false; if (search && !((u.name || '') + u.email).toLowerCase().includes(search.toLowerCase())) return false; return true; }); const counts = { all: users.length, admin: users.filter(u => u.role === 'admin').length, operator: users.filter(u => u.role === 'operator').length, viewer: users.filter(u => u.role === 'viewer').length, pending: users.filter(u => u.status === 'pending').length, }; const chipStyle = (on) => ({ flexShrink: 0, padding: '6px 10px', borderRadius: 9999, fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer', border: '1px solid ' + (on ? '#2563eb' : '#e2e8f0'), background: on ? '#eff6ff' : '#fff', color: on ? '#1d4ed8' : '#64748b', fontFamily: 'inherit', }); return (
{counts.all} {counts.pending > 0 && <>· {counts.pending} 承認待ち}
setSearch(e.target.value)} placeholder="名前・メールで検索..." style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: '#0f172a', minWidth: 0 }} />
{counts.pending > 0 && }
{filtered.map(u => onSelect(u.id)} />)} {filtered.length === 0 && ( (search || filter !== 'all') ? ( } title="該当するユーザーがいません" hint="検索やフィルタを変えてみてください。" action={ } /> ) : ( } title="ユーザーがいません" hint="右上の「ユーザーを招待」から追加できます。" /> ) )}
); } function UserDetail({ user, onPatch, onDelete, onApprove }) { if (!user) { return (
} title="ユーザーを選択してください" hint="左のリストから表示・編集したいユーザーを開きます。" />
); } const statusTone = USER_STATUS_TONE[user.status] || USER_STATUS_TONE.active; return (
{/* Header */}
{user.name || '(未設定)'}
{statusTone.label}
{user.email}
{user.status === 'pending' && ( )}
{/* Body */}
{/* Summary strip */}
{/* Role & permissions */}
ロールと権限
{[ { id: 'admin', label: 'Admin', desc: '全ての設定変更・ユーザー管理・システム操作' }, { id: 'operator', label: 'Operator', desc: 'タスク作成・実行・スケジュール管理' }, { id: 'viewer', label: 'Viewer', desc: '閲覧のみ。タスクの作成・変更不可' }, ].map(r => ( ))}
{/* Profile */}
プロフィール
onPatch(user.id, { name: e.target.value })} />
onPatch(user.id, { timezone: e.target.value })}> onPatch(user.id, { defaultPiece: e.target.value })}>
); } function UsersPage({ users, activeId, setActiveId, onPatch, onDelete, onApprove, onOpenInvite }) { const [filter, setFilter] = React.useState('all'); const [search, setSearch] = React.useState(''); const active = users.find(u => u.id === activeId) || users[0]; return (
); } window.UsersPage = UsersPage;