|
@@ -1,7 +1,48 @@
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import React, { useState, useEffect } from 'react';
|
|
|
import { ModelConfig, ModelType, AppSettings, KnowledgeGroup, Tenant, TenantMember } from '../../types';
|
|
import { ModelConfig, ModelType, AppSettings, KnowledgeGroup, Tenant, TenantMember } from '../../types';
|
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
|
-import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Users, Shield, Key, LogOut, Globe, Search, Settings as SettingsIcon, ToggleLeft, ToggleRight, Database, Sparkles, ChevronRight, ChevronDown, Lock, Building2, BookOpen, UserCircle, HardDrive, LayoutGrid } from 'lucide-react';
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ ChevronLeft,
|
|
|
|
|
+ ChevronRight,
|
|
|
|
|
+ Plus,
|
|
|
|
|
+ Search,
|
|
|
|
|
+ KeyRound,
|
|
|
|
|
+ Trash2,
|
|
|
|
|
+ Edit,
|
|
|
|
|
+ UserPlus,
|
|
|
|
|
+ Globe,
|
|
|
|
|
+ PlusCircle,
|
|
|
|
|
+ Clock,
|
|
|
|
|
+ ExternalLink,
|
|
|
|
|
+ Download,
|
|
|
|
|
+ Upload,
|
|
|
|
|
+ Building,
|
|
|
|
|
+ Settings as SettingsIcon,
|
|
|
|
|
+ Shield,
|
|
|
|
|
+ User,
|
|
|
|
|
+ MoreVertical,
|
|
|
|
|
+ Check,
|
|
|
|
|
+ ChevronDown,
|
|
|
|
|
+ ChevronUp,
|
|
|
|
|
+ Filter,
|
|
|
|
|
+ RefreshCcw,
|
|
|
|
|
+ LayoutDashboard,
|
|
|
|
|
+ Users,
|
|
|
|
|
+ Database,
|
|
|
|
|
+ UserCircle,
|
|
|
|
|
+ HardDrive,
|
|
|
|
|
+ LayoutGrid,
|
|
|
|
|
+ X,
|
|
|
|
|
+ Key,
|
|
|
|
|
+ Loader2,
|
|
|
|
|
+ Edit2,
|
|
|
|
|
+ Save,
|
|
|
|
|
+ Cpu,
|
|
|
|
|
+ BookOpen,
|
|
|
|
|
+ Sparkles,
|
|
|
|
|
+ ToggleRight,
|
|
|
|
|
+ ToggleLeft,
|
|
|
|
|
+} from "lucide-react";
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
import { userService } from '../../services/userService';
|
|
import { userService } from '../../services/userService';
|
|
|
import { settingsService } from '../../services/settingsService';
|
|
import { settingsService } from '../../services/settingsService';
|
|
@@ -46,6 +87,56 @@ const buildTenantTree = (tenants: Tenant[]): Tenant[] => {
|
|
|
return roots;
|
|
return roots;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// Moved outside to prevent re-mounting
|
|
|
|
|
+const Pagination: React.FC<{
|
|
|
|
|
+ current: number;
|
|
|
|
|
+ total: number;
|
|
|
|
|
+ pageSize: number;
|
|
|
|
|
+ onChange: (page: number) => void;
|
|
|
|
|
+}> = ({ current, total, pageSize, onChange }) => {
|
|
|
|
|
+ const totalPages = Math.ceil(total / pageSize);
|
|
|
|
|
+ if (totalPages <= 1) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center justify-center gap-2 mt-6">
|
|
|
|
|
+ <button
|
|
|
|
|
+ disabled={current === 1}
|
|
|
|
|
+ onClick={() => onChange(current - 1)}
|
|
|
|
|
+ className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <ChevronDown className="w-4 h-4 rotate-90" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div className="flex items-center gap-1">
|
|
|
|
|
+ {[...Array(totalPages)].map((_, i) => {
|
|
|
|
|
+ const p = i + 1;
|
|
|
|
|
+ if (totalPages > 7) {
|
|
|
|
|
+ if (p !== 1 && p !== totalPages && Math.abs(p - current) > 1) {
|
|
|
|
|
+ if (p === 2 || p === totalPages - 1) return <span key={p} className="px-1 font-bold text-slate-300">...</span>;
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={p}
|
|
|
|
|
+ onClick={() => onChange(p)}
|
|
|
|
|
+ className={`w-9 h-9 flex items-center justify-center rounded-xl text-xs font-black transition-all ${current === p ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border border-slate-200 text-slate-500 hover:border-indigo-500 hover:text-indigo-600'}`}
|
|
|
|
|
+ >
|
|
|
|
|
+ {p}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ disabled={current === totalPages}
|
|
|
|
|
+ onClick={() => onChange(current + 1)}
|
|
|
|
|
+ className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
|
|
|
+ >
|
|
|
|
|
+ <ChevronDown className="w-4 h-4 -rotate-90" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
models,
|
|
models,
|
|
|
authToken,
|
|
authToken,
|
|
@@ -99,6 +190,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
// --- Manage Members Modal State ---
|
|
// --- Manage Members Modal State ---
|
|
|
const [managingMembersTenantId, setManagingMembersTenantId] = useState<string | null>(null);
|
|
const [managingMembersTenantId, setManagingMembersTenantId] = useState<string | null>(null);
|
|
|
const [tenantMembers, setTenantMembers] = useState<any[]>([]);
|
|
const [tenantMembers, setTenantMembers] = useState<any[]>([]);
|
|
|
|
|
+ const [allMemberIds, setAllMemberIds] = useState<Set<string>>(new Set());
|
|
|
const [memberUserSearch, setMemberUserSearch] = useState('');
|
|
const [memberUserSearch, setMemberUserSearch] = useState('');
|
|
|
const [bindingRole, setBindingRole] = useState('USER');
|
|
const [bindingRole, setBindingRole] = useState('USER');
|
|
|
const [currentMemberSearch, setCurrentMemberSearch] = useState('');
|
|
const [currentMemberSearch, setCurrentMemberSearch] = useState('');
|
|
@@ -106,7 +198,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
const [activeTenantManagementId, setActiveTenantManagementId] = useState<string | null>(null);
|
|
const [activeTenantManagementId, setActiveTenantManagementId] = useState<string | null>(null);
|
|
|
const [memberPage, setMemberPage] = useState(1);
|
|
const [memberPage, setMemberPage] = useState(1);
|
|
|
const [memberTotal, setMemberTotal] = useState(0);
|
|
const [memberTotal, setMemberTotal] = useState(0);
|
|
|
- const MEMBER_PAGE_SIZE = 10;
|
|
|
|
|
|
|
+ const MEMBER_PAGE_SIZE = 20;
|
|
|
|
|
|
|
|
const [userTotal, setUserTotal] = useState(0);
|
|
const [userTotal, setUserTotal] = useState(0);
|
|
|
|
|
|
|
@@ -122,9 +214,6 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
parentId: null
|
|
parentId: null
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- setMemberPage(1);
|
|
|
|
|
- }, [activeTenantManagementId, currentMemberSearch]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -134,76 +223,34 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}, [initialTab]);
|
|
}, [initialTab]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- if (activeTab === 'user') {
|
|
|
|
|
- fetchUsers();
|
|
|
|
|
|
|
+ if (activeTab === 'user' || activeTab === 'tenants') {
|
|
|
|
|
+ fetchUsers(userPage);
|
|
|
}
|
|
}
|
|
|
}, [userPage]);
|
|
}, [userPage]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (selectedTenantId) {
|
|
if (selectedTenantId) {
|
|
|
- fetchTenantMembers(selectedTenantId);
|
|
|
|
|
|
|
+ fetchTenantMembers(selectedTenantId, memberPage);
|
|
|
|
|
+ fetchAllMemberIds(selectedTenantId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setAllMemberIds(new Set());
|
|
|
}
|
|
}
|
|
|
- }, [memberPage, selectedTenantId]);
|
|
|
|
|
|
|
+ }, [selectedTenantId, memberPage]);
|
|
|
|
|
|
|
|
- const Pagination: React.FC<{
|
|
|
|
|
- current: number;
|
|
|
|
|
- total: number;
|
|
|
|
|
- pageSize: number;
|
|
|
|
|
- onChange: (page: number) => void;
|
|
|
|
|
- }> = ({ current, total, pageSize, onChange }) => {
|
|
|
|
|
- const totalPages = Math.ceil(total / pageSize);
|
|
|
|
|
- if (totalPages <= 1) return null;
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="flex items-center justify-center gap-2 mt-6">
|
|
|
|
|
- <button
|
|
|
|
|
- disabled={current === 1}
|
|
|
|
|
- onClick={() => onChange(current - 1)}
|
|
|
|
|
- className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- <ChevronDown className="w-4 h-4 rotate-90" />
|
|
|
|
|
- </button>
|
|
|
|
|
- <div className="flex items-center gap-1">
|
|
|
|
|
- {[...Array(totalPages)].map((_, i) => {
|
|
|
|
|
- const p = i + 1;
|
|
|
|
|
- if (totalPages > 7) {
|
|
|
|
|
- if (p !== 1 && p !== totalPages && Math.abs(p - current) > 1) {
|
|
|
|
|
- if (p === 2 || p === totalPages - 1) return <span key={p} className="px-1 font-bold text-slate-300">...</span>;
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return (
|
|
|
|
|
- <button
|
|
|
|
|
- key={p}
|
|
|
|
|
- onClick={() => onChange(p)}
|
|
|
|
|
- className={`w-9 h-9 flex items-center justify-center rounded-xl text-xs font-black transition-all ${current === p ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border border-slate-200 text-slate-500 hover:border-indigo-500 hover:text-indigo-600'}`}
|
|
|
|
|
- >
|
|
|
|
|
- {p}
|
|
|
|
|
- </button>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- <button
|
|
|
|
|
- disabled={current === totalPages}
|
|
|
|
|
- onClick={() => onChange(current + 1)}
|
|
|
|
|
- className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- <ChevronDown className="w-4 h-4 -rotate-90" />
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // ユーザー一覧の取得(ユーザータブがアクティブな場合)
|
|
|
|
|
// Data fetching on tab change
|
|
// Data fetching on tab change
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
|
|
+ // Reset pages when switching tabs to avoid bleed-over
|
|
|
|
|
+ if (activeTab === 'user' || activeTab === 'tenants') {
|
|
|
|
|
+ setUserPage(1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (activeTab === 'user') {
|
|
if (activeTab === 'user') {
|
|
|
- fetchUsers();
|
|
|
|
|
|
|
+ fetchUsers(1);
|
|
|
} else if (activeTab === 'general') {
|
|
} else if (activeTab === 'general') {
|
|
|
fetchSettingsAndGroups();
|
|
fetchSettingsAndGroups();
|
|
|
} else if (activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN') {
|
|
} else if (activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN') {
|
|
|
fetchTenantsData();
|
|
fetchTenantsData();
|
|
|
- fetchUsers(); // Ensure users are loaded for admin binding
|
|
|
|
|
|
|
+ fetchUsers(1); // Ensure users are loaded for admin binding
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Independent check for KB/Model settings to avoid being blocked by the branches above
|
|
// Independent check for KB/Model settings to avoid being blocked by the branches above
|
|
@@ -318,10 +365,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- ユーザータブのハンドラー ---
|
|
// --- ユーザータブのハンドラー ---
|
|
|
- const fetchUsers = async () => {
|
|
|
|
|
|
|
+ const fetchUsers = async (page?: number) => {
|
|
|
setIsUserLoading(true);
|
|
setIsUserLoading(true);
|
|
|
|
|
+ const p = page || userPage;
|
|
|
try {
|
|
try {
|
|
|
- const result = await userService.getUsers(userPage, USER_PAGE_SIZE);
|
|
|
|
|
|
|
+ const result = await userService.getUsers(p, USER_PAGE_SIZE);
|
|
|
if (result && result.data) {
|
|
if (result && result.data) {
|
|
|
setUsers(result.data);
|
|
setUsers(result.data);
|
|
|
setUserTotal(result.total);
|
|
setUserTotal(result.total);
|
|
@@ -393,10 +441,22 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const fetchTenantMembers = async (tenantId: string) => {
|
|
|
|
|
|
|
+ const fetchAllMemberIds = async (tenantId: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await apiClient.get<string[]>(`/v1/tenants/${tenantId}/members/ids`);
|
|
|
|
|
+ if (Array.isArray(data)) {
|
|
|
|
|
+ setAllMemberIds(new Set(data));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('Failed to fetch all member IDs:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchTenantMembers = async (tenantId: string, page?: number) => {
|
|
|
setIsMembersLoading(true);
|
|
setIsMembersLoading(true);
|
|
|
|
|
+ const p = page || memberPage;
|
|
|
try {
|
|
try {
|
|
|
- const { data } = await apiClient.get(`/v1/tenants/${tenantId}/members?page=${memberPage}&limit=${MEMBER_PAGE_SIZE}`);
|
|
|
|
|
|
|
+ const { data } = await apiClient.get(`/v1/tenants/${tenantId}/members?page=${p}&limit=${MEMBER_PAGE_SIZE}`);
|
|
|
if (data && data.data) {
|
|
if (data && data.data) {
|
|
|
setTenantMembers(data.data);
|
|
setTenantMembers(data.data);
|
|
|
setMemberTotal(data.total);
|
|
setMemberTotal(data.total);
|
|
@@ -411,9 +471,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const handleAddMember = async (tenantId: string, userId: string, role: string) => {
|
|
|
|
|
|
|
+ const handleAddMember = async (tenantId: string, userId: string, role: string = 'USER') => {
|
|
|
try {
|
|
try {
|
|
|
- await apiClient.post(`/v1/tenants/${tenantId}/members`, { userId, role: bindingRole });
|
|
|
|
|
|
|
+ await apiClient.post(`/v1/tenants/${tenantId}/members`, { userId, role });
|
|
|
|
|
+ setAllMemberIds(prev => {
|
|
|
|
|
+ const next = new Set(prev);
|
|
|
|
|
+ next.add(userId);
|
|
|
|
|
+ return next;
|
|
|
|
|
+ });
|
|
|
showSuccess(t('confirm'));
|
|
showSuccess(t('confirm'));
|
|
|
fetchTenantMembers(tenantId);
|
|
fetchTenantMembers(tenantId);
|
|
|
fetchTenantsData();
|
|
fetchTenantsData();
|
|
@@ -425,6 +490,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
const handleRemoveMember = async (tenantId: string, userId: string) => {
|
|
const handleRemoveMember = async (tenantId: string, userId: string) => {
|
|
|
try {
|
|
try {
|
|
|
await apiClient.delete(`/v1/tenants/${tenantId}/members/${userId}`);
|
|
await apiClient.delete(`/v1/tenants/${tenantId}/members/${userId}`);
|
|
|
|
|
+ setAllMemberIds(prev => {
|
|
|
|
|
+ const next = new Set(prev);
|
|
|
|
|
+ next.delete(userId);
|
|
|
|
|
+ return next;
|
|
|
|
|
+ });
|
|
|
showSuccess('User removed from organization');
|
|
showSuccess('User removed from organization');
|
|
|
fetchTenantMembers(tenantId);
|
|
fetchTenantMembers(tenantId);
|
|
|
fetchTenantsData();
|
|
fetchTenantsData();
|
|
@@ -530,6 +600,43 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleExportUsers = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const blob = await userService.exportUsers();
|
|
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
|
|
+ const a = document.createElement('a');
|
|
|
|
|
+ a.href = url;
|
|
|
|
|
+ a.download = `users_${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
|
|
|
+ document.body.appendChild(a);
|
|
|
|
|
+ a.click();
|
|
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
|
|
+ document.body.removeChild(a);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Export users failed', error);
|
|
|
|
|
+ showError(t('exportFailed'));
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleImportUsers = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ const file = e.target.files?.[0];
|
|
|
|
|
+ if (!file) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await userService.importUsers(file);
|
|
|
|
|
+ showSuccess(t('importSuccess').replace('$1', (result.created + result.updated).toString()).replace('$2', result.errors.length.toString()));
|
|
|
|
|
+ fetchUsers();
|
|
|
|
|
+ if (result.errors.length > 0) {
|
|
|
|
|
+ console.warn('Import had errors:', result.errors);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('Import users failed', error);
|
|
|
|
|
+ showError(t('importFailed') + (error.response?.data?.message ? `: ${error.response.data.message}` : ''));
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // Reset input
|
|
|
|
|
+ e.target.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleSaveModel = async () => {
|
|
const handleSaveModel = async () => {
|
|
|
if (!authToken) return;
|
|
if (!authToken) return;
|
|
|
setIsLoading(true);
|
|
setIsLoading(true);
|
|
@@ -596,7 +703,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
) : (
|
|
) : (
|
|
|
<div className="w-5" />
|
|
<div className="w-5" />
|
|
|
)}
|
|
)}
|
|
|
- <Building2 size={16} className={isSelected ? 'text-white' : 'text-slate-400'} />
|
|
|
|
|
|
|
+ <Building size={16} className={isSelected ? 'text-white' : 'text-slate-400'} />
|
|
|
<span className="text-sm font-bold truncate">{tenant.name}</span>
|
|
<span className="text-sm font-bold truncate">{tenant.name}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -736,13 +843,38 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<p className="text-xs text-slate-400 font-medium">{''}</p>
|
|
<p className="text-xs text-slate-400 font-medium">{''}</p>
|
|
|
</div>
|
|
</div>
|
|
|
{currentUser?.role === 'SUPER_ADMIN' && (
|
|
{currentUser?.role === 'SUPER_ADMIN' && (
|
|
|
- <button
|
|
|
|
|
- onClick={() => setShowAddUser(!showAddUser)}
|
|
|
|
|
- className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
|
|
|
|
|
- >
|
|
|
|
|
- <Plus className="w-4 h-4" />
|
|
|
|
|
- {t('addUser')}
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={handleExportUsers}
|
|
|
|
|
+ className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 text-slate-600 text-sm font-bold rounded-2xl hover:bg-slate-50 shadow-sm transition-all active:scale-95"
|
|
|
|
|
+ title={t('exportUsers')}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Download className="w-4 h-4" />
|
|
|
|
|
+ <span className="hidden sm:inline">{t('exportUsers')}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ accept=".xlsx,.xls,.csv"
|
|
|
|
|
+ onChange={handleImportUsers}
|
|
|
|
|
+ className="absolute inset-0 opacity-0 cursor-pointer"
|
|
|
|
|
+ title={t('importUsers')}
|
|
|
|
|
+ />
|
|
|
|
|
+ <button
|
|
|
|
|
+ className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 text-slate-600 text-sm font-bold rounded-2xl hover:bg-slate-50 shadow-sm transition-all active:scale-95"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Upload className="w-4 h-4" />
|
|
|
|
|
+ <span className="hidden sm:inline">{t('importUsers')}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setShowAddUser(!showAddUser)}
|
|
|
|
|
+ className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Plus className="w-4 h-4" />
|
|
|
|
|
+ {t('addUser')}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -899,7 +1031,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody className="divide-y divide-slate-100">
|
|
<tbody className="divide-y divide-slate-100">
|
|
|
<AnimatePresence>
|
|
<AnimatePresence>
|
|
|
- {users.map((user, index) => {
|
|
|
|
|
|
|
+ {users.filter((u: any) => u.username !== 'admin').map((user: any, index: number) => {
|
|
|
let IconComponent = User;
|
|
let IconComponent = User;
|
|
|
let iconColors = 'bg-slate-50 text-slate-400';
|
|
let iconColors = 'bg-slate-50 text-slate-400';
|
|
|
if (user.isAdmin) {
|
|
if (user.isAdmin) {
|
|
@@ -938,7 +1070,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
key={m.tenantId}
|
|
key={m.tenantId}
|
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 text-[9px] font-black rounded-md uppercase tracking-wider border border-emerald-100"
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 text-[9px] font-black rounded-md uppercase tracking-wider border border-emerald-100"
|
|
|
>
|
|
>
|
|
|
- <Building2 size={8} />
|
|
|
|
|
|
|
+ <Building size={8} />
|
|
|
{m.tenant?.name || m.tenantId}
|
|
{m.tenant?.name || m.tenantId}
|
|
|
</span>
|
|
</span>
|
|
|
))}
|
|
))}
|
|
@@ -1041,8 +1173,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
tenant={t}
|
|
tenant={t}
|
|
|
selectedTenantId={selectedTenantId}
|
|
selectedTenantId={selectedTenantId}
|
|
|
onSelect={(id) => {
|
|
onSelect={(id) => {
|
|
|
- setSelectedTenantId(id);
|
|
|
|
|
- fetchTenantMembers(id);
|
|
|
|
|
|
|
+ if (id !== selectedTenantId) {
|
|
|
|
|
+ setSelectedTenantId(id);
|
|
|
|
|
+ setMemberPage(1);
|
|
|
|
|
+ setUserPage(1);
|
|
|
|
|
+ }
|
|
|
}}
|
|
}}
|
|
|
onCreateSubtenant={(parentId) => {
|
|
onCreateSubtenant={(parentId) => {
|
|
|
setNewTenant({ name: '', domain: '', parentId });
|
|
setNewTenant({ name: '', domain: '', parentId });
|
|
@@ -1053,7 +1188,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
))
|
|
))
|
|
|
) : (
|
|
) : (
|
|
|
<div className="py-20 text-center">
|
|
<div className="py-20 text-center">
|
|
|
- <Building2 size={32} className="mx-auto text-slate-200 mb-3" />
|
|
|
|
|
|
|
+ <Building size={32} className="mx-auto text-slate-200 mb-3" />
|
|
|
<p className="text-xs font-black text-slate-300 uppercase tracking-widest">{t('noOrganizations')}</p>
|
|
<p className="text-xs font-black text-slate-300 uppercase tracking-widest">{t('noOrganizations')}</p>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
@@ -1062,7 +1197,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="p-4 bg-slate-50 border-t border-slate-100 shrink-0">
|
|
<div className="p-4 bg-slate-50 border-t border-slate-100 shrink-0">
|
|
|
<div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-2xl shadow-sm">
|
|
<div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-2xl shadow-sm">
|
|
|
<div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
|
|
<div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
|
|
|
- <Building2 size={20} />
|
|
|
|
|
|
|
+ <Building size={20} />
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('totalTenants')}</p>
|
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('totalTenants')}</p>
|
|
@@ -1080,7 +1215,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="p-8 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between shrink-0">
|
|
<div className="p-8 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between shrink-0">
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-4">
|
|
|
<div className="w-14 h-14 rounded-2xl bg-white border border-slate-200 shadow-sm flex items-center justify-center text-indigo-600">
|
|
<div className="w-14 h-14 rounded-2xl bg-white border border-slate-200 shadow-sm flex items-center justify-center text-indigo-600">
|
|
|
- <Building2 size={28} />
|
|
|
|
|
|
|
+ <Building size={28} />
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-2">
|
|
@@ -1123,7 +1258,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="p-6 border-b border-slate-50 flex items-center justify-between shrink-0">
|
|
<div className="p-6 border-b border-slate-50 flex items-center justify-between shrink-0">
|
|
|
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest">{t('orgMembers')}</h4>
|
|
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest">{t('orgMembers')}</h4>
|
|
|
<span className="text-[10px] font-black px-2 py-0.5 bg-slate-100 text-slate-500 rounded-full">
|
|
<span className="text-[10px] font-black px-2 py-0.5 bg-slate-100 text-slate-500 rounded-full">
|
|
|
- {t('membersCount').replace('$1', (tenantMembers?.length || 0).toString())}
|
|
|
|
|
|
|
+ {t('membersCount').replace('$1', (memberTotal || 0).toString())}
|
|
|
</span>
|
|
</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex-1 overflow-y-auto p-6 scrollbar-hide">
|
|
<div className="flex-1 overflow-y-auto p-6 scrollbar-hide">
|
|
@@ -1201,24 +1336,40 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
|
|
{users
|
|
{users
|
|
|
.filter(u =>
|
|
.filter(u =>
|
|
|
- !tenantMembers?.some((m: any) => m.userId === u.id) &&
|
|
|
|
|
|
|
+ u.username !== 'admin' &&
|
|
|
u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
|
|
u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
|
|
|
)
|
|
)
|
|
|
- .map(u => (
|
|
|
|
|
- <button
|
|
|
|
|
- key={u.id}
|
|
|
|
|
- onClick={() => handleAddMember(activeTenant.id, u.id, bindingRole)}
|
|
|
|
|
- className="w-full p-3 bg-white border border-slate-100 rounded-xl flex items-center justify-between group hover:border-indigo-500 hover:shadow-sm transition-all"
|
|
|
|
|
- >
|
|
|
|
|
- <div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
- <div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors">
|
|
|
|
|
- <User size={14} />
|
|
|
|
|
|
|
+ .map(u => {
|
|
|
|
|
+ const isAlreadyMember = allMemberIds.has(u.id);
|
|
|
|
|
+ return (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={u.id}
|
|
|
|
|
+ onClick={() => !isAlreadyMember && handleAddMember(activeTenant.id, u.id, bindingRole)}
|
|
|
|
|
+ disabled={isAlreadyMember}
|
|
|
|
|
+ className={`w-full p-3 border rounded-xl flex items-center justify-between group transition-all ${isAlreadyMember
|
|
|
|
|
+ ? 'bg-slate-50 border-slate-100 cursor-not-allowed opacity-60'
|
|
|
|
|
+ : 'bg-white border-slate-100 hover:border-indigo-500 hover:shadow-sm'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors ${isAlreadyMember
|
|
|
|
|
+ ? 'bg-slate-100 text-slate-300'
|
|
|
|
|
+ : 'bg-slate-50 text-slate-400 group-hover:bg-indigo-50 group-hover:text-indigo-600'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ <User size={14} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span className={`text-[13px] font-bold truncate ${isAlreadyMember ? 'text-slate-400' : 'text-slate-700'}`}>
|
|
|
|
|
+ {u.username}
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
- <span className="text-[13px] font-bold text-slate-700 truncate">{u.username}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Plus size={14} className="text-slate-300 group-hover:text-indigo-600" />
|
|
|
|
|
- </button>
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ {isAlreadyMember ? (
|
|
|
|
|
+ <Check size={14} className="text-emerald-500" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Plus size={14} className="text-slate-300 group-hover:text-indigo-600" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ );
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
<Pagination
|
|
<Pagination
|
|
|
current={userPage}
|
|
current={userPage}
|
|
@@ -1276,7 +1427,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
) : (
|
|
) : (
|
|
|
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center bg-slate-50/30">
|
|
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center bg-slate-50/30">
|
|
|
<div className="w-24 h-24 rounded-[2rem] bg-indigo-50 flex items-center justify-center text-indigo-400 mb-6 shadow-xl shadow-indigo-100/50">
|
|
<div className="w-24 h-24 rounded-[2rem] bg-indigo-50 flex items-center justify-center text-indigo-400 mb-6 shadow-xl shadow-indigo-100/50">
|
|
|
- <Building2 size={48} />
|
|
|
|
|
|
|
+ <Building size={48} />
|
|
|
</div>
|
|
</div>
|
|
|
<h2 className="text-2xl font-black text-slate-900 tracking-tight mb-2">{t('selectOrg')}</h2>
|
|
<h2 className="text-2xl font-black text-slate-900 tracking-tight mb-2">{t('selectOrg')}</h2>
|
|
|
<p className="text-sm text-slate-400 max-w-xs font-medium leading-relaxed">{t('selectOrgDesc')}</p>
|
|
<p className="text-sm text-slate-400 max-w-xs font-medium leading-relaxed">{t('selectOrgDesc')}</p>
|
|
@@ -1879,16 +2030,18 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
<SettingsIcon size={18} />
|
|
<SettingsIcon size={18} />
|
|
|
{t('generalSettings')}
|
|
{t('generalSettings')}
|
|
|
</button>
|
|
</button>
|
|
|
- {(currentUser?.role === 'TENANT_ADMIN' || currentUser?.role === 'SUPER_ADMIN' || isAdmin) && (
|
|
|
|
|
|
|
+ {currentUser?.role === 'SUPER_ADMIN' && (
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setActiveTab('user')}
|
|
|
|
|
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'user' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <UserCircle size={18} />
|
|
|
|
|
+ {t('userManagement')}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {isAdmin && (
|
|
|
<>
|
|
<>
|
|
|
- <button
|
|
|
|
|
- onClick={() => setActiveTab('user')}
|
|
|
|
|
- className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'user' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- <UserCircle size={18} />
|
|
|
|
|
- {t('userManagement')}
|
|
|
|
|
- </button>
|
|
|
|
|
<button
|
|
<button
|
|
|
onClick={() => setActiveTab('model')}
|
|
onClick={() => setActiveTab('model')}
|
|
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'model' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
|
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'model' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
|
|
@@ -1960,9 +2113,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
|
|
transition={{ duration: 0.3 }}
|
|
transition={{ duration: 0.3 }}
|
|
|
>
|
|
>
|
|
|
{activeTab === 'general' && renderGeneralTab()}
|
|
{activeTab === 'general' && renderGeneralTab()}
|
|
|
- {activeTab === 'user' && renderUserTab()}
|
|
|
|
|
- {activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderModelTab()}
|
|
|
|
|
- {activeTab === 'knowledge_base' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderKnowledgeBaseTab()}
|
|
|
|
|
|
|
+ {activeTab === 'user' && currentUser?.role === 'SUPER_ADMIN' && renderUserTab()}
|
|
|
|
|
+ {activeTab === 'model' && isAdmin && renderModelTab()}
|
|
|
|
|
+ {activeTab === 'knowledge_base' && isAdmin && renderKnowledgeBaseTab()}
|
|
|
{activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN' && renderTenantsTab()}
|
|
{activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN' && renderTenantsTab()}
|
|
|
</motion.div>
|
|
</motion.div>
|
|
|
</AnimatePresence>
|
|
</AnimatePresence>
|