1040 lines
52 KiB
TypeScript
1040 lines
52 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { inventoryApi } from '@/lib/api';
|
|
import PageShell from '@/components/PageShell';
|
|
import StatCard from '@/components/StatCard';
|
|
import {
|
|
Shield,
|
|
ShieldAlert,
|
|
UserPlus,
|
|
User,
|
|
Trash2,
|
|
Tag,
|
|
Plus,
|
|
AlertTriangle,
|
|
LogOut,
|
|
HardDrive,
|
|
Download,
|
|
RotateCcw,
|
|
FileText,
|
|
Upload,
|
|
Brain,
|
|
Globe,
|
|
Edit2,
|
|
Server,
|
|
Wifi,
|
|
WifiOff,
|
|
Clock,
|
|
History,
|
|
Database,
|
|
Layers,
|
|
X,
|
|
ChevronDown,
|
|
Power,
|
|
Lock,
|
|
Cpu,
|
|
Zap
|
|
} from 'lucide-react';
|
|
import { toast } from 'react-hot-toast';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export default function AdminPage() {
|
|
const [users, setUsers] = useState<any[]>([]);
|
|
const [categories, setCategories] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// LDAP Config State
|
|
const [ldapConfig, setLdapConfig] = useState<any>({
|
|
ldap_enabled: false,
|
|
server_uri: '',
|
|
base_dn: '',
|
|
user_template: '',
|
|
groups_dn: '',
|
|
use_tls: false,
|
|
role_mappings: []
|
|
});
|
|
const [testingLdap, setTestingLdap] = useState(false);
|
|
|
|
// Edit States
|
|
const [editingUser, setEditingUser] = useState<any | null>(null);
|
|
const [editUserForm, setEditUserForm] = useState({ username: '', password: '', role: 'user' });
|
|
|
|
const [editingCategory, setEditingCategory] = useState<any | null>(null);
|
|
const [editCatForm, setEditCatForm] = useState({ name: '', description: '' });
|
|
|
|
// DB Management State
|
|
const [backups, setBackups] = useState<any[]>([]);
|
|
const [dbStats, setDbStats] = useState({ backup_count: 0, total_size_bytes: 0 });
|
|
const [dbSettings, setDbSettings] = useState({ retention_count: 10, schedule_hour: 3, schedule_freq_days: 1 });
|
|
const [isBackingUp, setIsBackingUp] = useState(false);
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
|
|
// AI Prompt & Config State
|
|
const [aiPrompt, setAiPrompt] = useState("");
|
|
const [aiConfig, setAiConfig] = useState<any>(null);
|
|
const [aiKeys, setAiKeys] = useState({ gemini: '', claude: '' });
|
|
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
|
|
const [isSavingKeys, setIsSavingKeys] = useState(false);
|
|
const [isTestingKeys, setIsTestingKeys] = useState<{gemini: boolean, claude: boolean}>({gemini: false, claude: false});
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [u, c, l, b, s, st] = await Promise.all([
|
|
inventoryApi.getUsers(),
|
|
inventoryApi.getCategories(),
|
|
inventoryApi.getLdapConfig(),
|
|
inventoryApi.getDbBackups(),
|
|
inventoryApi.getDbStats(),
|
|
inventoryApi.getDbSettings()
|
|
]);
|
|
setUsers(u);
|
|
setCategories(c);
|
|
if (l && l.server_uri) setLdapConfig(l);
|
|
setBackups(b);
|
|
setDbStats(s);
|
|
setDbSettings(st);
|
|
|
|
const [p, ai] = await Promise.all([
|
|
inventoryApi.getAiPrompt(),
|
|
inventoryApi.getAiConfig()
|
|
]);
|
|
setAiPrompt(p.value);
|
|
setAiConfig(ai);
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
toast.error("Failed to load admin data");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddUser = async () => {
|
|
const name = prompt("Enter new username:");
|
|
if (!name) return;
|
|
const pwd = prompt("Enter password for " + name + ":");
|
|
if (!pwd) return;
|
|
|
|
try {
|
|
await inventoryApi.createUser({ username: name, password: pwd, role: 'user' });
|
|
toast.success("User created successfully");
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Failed to create user");
|
|
}
|
|
};
|
|
|
|
const handleDeleteUser = async (id: number, username: string) => {
|
|
if (username === 'Admin') return;
|
|
if (!confirm(`Delete user ${username}?`)) return;
|
|
|
|
try {
|
|
await inventoryApi.deleteUser(id);
|
|
toast.success("User removed");
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Delete failed");
|
|
}
|
|
};
|
|
|
|
const handleUpdateAiProvider = async (provider: string) => {
|
|
try {
|
|
await inventoryApi.updateAiProvider(provider);
|
|
toast.success(`AI provider switched to ${provider}`);
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Failed to switch AI provider");
|
|
}
|
|
};
|
|
|
|
const handleSaveAiKeys = async () => {
|
|
if (!aiKeys.gemini && !aiKeys.claude) {
|
|
toast.error("Enter at least one API key to save");
|
|
return;
|
|
}
|
|
|
|
setIsSavingKeys(true);
|
|
try {
|
|
await inventoryApi.updateAiKeys({
|
|
gemini_api_key: aiKeys.gemini || undefined,
|
|
claude_api_key: aiKeys.claude || undefined
|
|
});
|
|
toast.success("AI Configuration keys updated successfully");
|
|
setAiKeys({ gemini: '', claude: '' });
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Failed to update AI keys");
|
|
} finally {
|
|
setIsSavingKeys(false);
|
|
}
|
|
};
|
|
|
|
const handleTestAiKey = async (provider: 'gemini' | 'claude') => {
|
|
const keyToTest = provider === 'gemini' ? aiKeys.gemini : aiKeys.claude;
|
|
|
|
setIsTestingKeys(prev => ({ ...prev, [provider]: true }));
|
|
try {
|
|
const res = await inventoryApi.testAiKey(provider, keyToTest);
|
|
toast.success(res.message || `${provider.toUpperCase()} connection successful!`);
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.detail || `Failed to test ${provider} key`;
|
|
toast.error(msg);
|
|
} finally {
|
|
setIsTestingKeys(prev => ({ ...prev, [provider]: false }));
|
|
}
|
|
};
|
|
|
|
const handleAddCategory = async () => {
|
|
const name = prompt("New category name:");
|
|
if (!name) return;
|
|
const desc = prompt("Description (optional):");
|
|
|
|
try {
|
|
await inventoryApi.createCategory({ name, description: desc });
|
|
toast.success("Category added");
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Failed to add category");
|
|
}
|
|
};
|
|
|
|
const handleUpdateCategorySubmit = async () => {
|
|
if (!editingCategory) return;
|
|
try {
|
|
await inventoryApi.updateCategory(editingCategory.id, editCatForm);
|
|
toast.success("Category updated");
|
|
setEditingCategory(null);
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Update failed");
|
|
}
|
|
};
|
|
|
|
const handleDeleteCategory = async (id: number, name: string) => {
|
|
if (!confirm(`Delete category ${name}?`)) return;
|
|
|
|
try {
|
|
await inventoryApi.deleteCategory(id);
|
|
toast.success("Category removed");
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error(err.response?.data?.detail || "Delete failed");
|
|
}
|
|
};
|
|
|
|
const handleUpdateUserSubmit = async () => {
|
|
if (!editingUser) return;
|
|
try {
|
|
const payload: any = { username: editUserForm.username, role: editUserForm.role };
|
|
if (editUserForm.password) payload.password = editUserForm.password;
|
|
|
|
await inventoryApi.updateUser(editingUser.id, payload);
|
|
toast.success("User updated successfully");
|
|
setEditingUser(null);
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Update failed");
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
import('@/lib/auth').then(m => m.clearAuth());
|
|
window.location.href = '/login';
|
|
};
|
|
|
|
const handleCreateBackup = async () => {
|
|
setIsBackingUp(true);
|
|
try {
|
|
await inventoryApi.triggerBackup();
|
|
toast.success("Snapshot created successfully");
|
|
loadData();
|
|
} catch (err: any) {
|
|
toast.error("Backup failed");
|
|
} finally {
|
|
setIsBackingUp(false);
|
|
}
|
|
};
|
|
|
|
const handleRestore = async (filename: string) => {
|
|
if (!confirm(`DANGEROUS: Restore database from ${filename}? Current data will be replaced. A rollback snapshot will be created automatically.`)) return;
|
|
|
|
const loadingToast = toast.loading("Restoring database...");
|
|
try {
|
|
await inventoryApi.restoreDatabase(filename);
|
|
toast.success("Database restored! Reloading system...", { id: loadingToast });
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} catch (err: any) {
|
|
toast.error("Restore failed", { id: loadingToast });
|
|
}
|
|
};
|
|
|
|
const handleUpdateDbSettings = async (newSettings: any) => {
|
|
try {
|
|
await inventoryApi.updateDbSettings(newSettings);
|
|
toast.success("System policy updated");
|
|
setDbSettings(newSettings);
|
|
} catch (err: any) {
|
|
toast.error("Failed to update settings");
|
|
}
|
|
};
|
|
|
|
const handleUpdatePrompt = async () => {
|
|
setIsSavingPrompt(true);
|
|
try {
|
|
await inventoryApi.updateAiPrompt(aiPrompt);
|
|
toast.success("AI Extraction Prompt updated");
|
|
} catch (err: any) {
|
|
toast.error("Failed to update AI prompt");
|
|
} finally {
|
|
setIsSavingPrompt(false);
|
|
}
|
|
};
|
|
|
|
const handleExportDb = async () => {
|
|
try {
|
|
const blob = await inventoryApi.exportDb();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `inventory_backup_${new Date().toISOString().split('T')[0]}.db`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
toast.success("Database exported successfully");
|
|
} catch (err: any) {
|
|
toast.error("Export failed");
|
|
}
|
|
};
|
|
|
|
const handleImportDb = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (!confirm("DANGEROUS: You are about to REPLACE the entire database with this file. All current data will be lost. Proceed?")) {
|
|
e.target.value = '';
|
|
return;
|
|
}
|
|
|
|
setIsImporting(true);
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const loadingToast = toast.loading("Importing database...");
|
|
try {
|
|
await inventoryApi.importDb(formData);
|
|
toast.success("Database imported! Reloading system...", { id: loadingToast });
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} catch (err: any) {
|
|
toast.error("Import failed: " + (err.response?.data?.detail || err.message), { id: loadingToast });
|
|
} finally {
|
|
setIsImporting(false);
|
|
e.target.value = '';
|
|
}
|
|
};
|
|
|
|
const handleUpdateLdap = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await inventoryApi.updateLdapConfig(ldapConfig);
|
|
toast.success("Enterprise configuration updated");
|
|
} catch (err: any) {
|
|
toast.error("Failed to update LDAP config");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTestLdap = async () => {
|
|
setTestingLdap(true);
|
|
try {
|
|
const res = await inventoryApi.testLdapConnection(ldapConfig);
|
|
if (res.status === 'success') {
|
|
toast.success(res.message || "LDAP Connection Successful!");
|
|
} else {
|
|
toast.error(`LDAP Error: ${res.message || "Unknown error"}`);
|
|
}
|
|
} catch (err: any) {
|
|
toast.error(`Connection failed: ${err.response?.data?.detail || err.message}`);
|
|
} finally {
|
|
setTestingLdap(false);
|
|
}
|
|
};
|
|
|
|
const formatSize = (bytes: number) => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
};
|
|
|
|
return (
|
|
<PageShell>
|
|
<div className="p-3 md:p-8 max-w-7xl mx-auto space-y-6 md:space-y-10 mb-20">
|
|
<header className="flex items-center gap-4 mb-8 md:mb-12">
|
|
<div className="p-3 md:p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20 shadow-xl shadow-indigo-500/5">
|
|
<Shield size={28} className="md:w-8 md:h-8" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-black tracking-tight text-white leading-tight">Admin Control</h1>
|
|
<p className="text-[10px] md:text-xs text-slate-500 font-bold uppercase tracking-widest mt-0.5">System Configuration & Security</p>
|
|
</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="ml-auto p-3 bg-slate-900 border border-slate-800 text-slate-500 hover:text-rose-500 rounded-2xl transition-all active:scale-95"
|
|
title="Secure Logout"
|
|
>
|
|
<LogOut size={20} />
|
|
</button>
|
|
</header>
|
|
|
|
<div className="grid lg:grid-cols-2 gap-6 md:gap-8 items-start">
|
|
{/* Main Controls - System Integrity */}
|
|
<section className="space-y-6 md:space-y-8">
|
|
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl overflow-hidden relative group transition-all">
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<Database size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">System Integrity</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
|
<div className="space-y-1 sm:pr-6">
|
|
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Database Health</p>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.3)]" />
|
|
<span className="text-sm font-bold text-slate-200">Operational</span>
|
|
</div>
|
|
</div>
|
|
<div className="sm:pl-6 sm:border-l border-slate-800">
|
|
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Last Backup</p>
|
|
<p className="text-sm font-bold text-slate-200 tabular-nums">{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}</p>
|
|
</div>
|
|
<div className="ml-auto">
|
|
<button
|
|
onClick={handleCreateBackup}
|
|
disabled={isBackingUp}
|
|
className="flex items-center gap-2 px-5 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-indigo-600/10 uppercase tracking-widest"
|
|
>
|
|
{isBackingUp ? <RotateCcw size={14} className="animate-spin" /> : <Database size={14} />}
|
|
{isBackingUp ? "Snapshotting..." : "Force Backup"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Identity Management */}
|
|
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 flex flex-col shadow-2xl transition-all group/identity">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<User size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">Identity Management</h2>
|
|
</div>
|
|
<button
|
|
onClick={handleAddUser}
|
|
className="flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 uppercase tracking-widest"
|
|
>
|
|
<UserPlus size={16} /> Add User
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-2.5 pr-2 -mr-2 overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
|
{users.map((user) => (
|
|
<div key={user.id} className="flex items-center justify-between p-3.5 bg-slate-950/40 border border-slate-800/40 rounded-2xl hover:border-primary/30 transition-all group">
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<div className={cn(
|
|
"w-9 h-9 rounded-xl flex items-center justify-center transition-colors shadow-inner shrink-0",
|
|
user.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-slate-500 border border-slate-700"
|
|
)}>
|
|
{user.role === 'admin' ? <Shield size={16} /> : <User size={16} />}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-bold text-slate-200 truncate">{user.username}</p>
|
|
<p className="text-[9px] text-slate-500 font-black uppercase tracking-widest">{user.role}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 ml-2">
|
|
<button
|
|
onClick={() => {
|
|
setEditingUser(user);
|
|
setEditUserForm({ username: user.username, password: '', role: user.role });
|
|
}}
|
|
className="p-2 text-slate-600 hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
|
|
>
|
|
<Edit2 size={16} />
|
|
</button>
|
|
{user.username !== 'Admin' && (
|
|
<button
|
|
onClick={() => handleDeleteUser(user.id, user.username)}
|
|
className="p-2 text-slate-600 hover:text-red-500 hover:bg-red-500/5 rounded-xl transition-all"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Right Column - LDAP & AI */}
|
|
<section className="space-y-6 md:space-y-8">
|
|
<div className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ldap">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<Globe size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">Enterprise LDAP</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-slate-950/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-indigo-500/30">
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
|
|
ldapConfig.ldap_enabled ? "bg-green-500/10 text-green-500" : "bg-slate-800 text-slate-500"
|
|
)}>
|
|
<Power size={14} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-black text-slate-200 uppercase tracking-widest leading-none">LDAP Authorization</p>
|
|
<p className={cn("text-[8px] font-bold mt-1", ldapConfig.ldap_enabled ? "text-green-500/80" : "text-slate-600")}>
|
|
{ldapConfig.ldap_enabled ? "External Directory Active" : "Internal Authentication Only"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setLdapConfig({...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled})}
|
|
className={cn(
|
|
"w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
|
|
ldapConfig.ldap_enabled ? "bg-indigo-600 border-indigo-500" : "bg-slate-800 border-slate-700"
|
|
)}
|
|
>
|
|
<div className={cn(
|
|
"absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
|
|
ldapConfig.ldap_enabled ? "right-0.5" : "left-0.5"
|
|
)} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Ldap Uri</label>
|
|
<div className="relative">
|
|
<Server className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="ldap://host:389"
|
|
value={ldapConfig.server_uri}
|
|
onChange={(e) => setLdapConfig({...ldapConfig, server_uri: e.target.value})}
|
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Context DN</label>
|
|
<div className="relative">
|
|
<Shield className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="dc=example,dc=com"
|
|
value={ldapConfig.base_dn}
|
|
onChange={(e) => setLdapConfig({...ldapConfig, base_dn: e.target.value})}
|
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">User DN Template</label>
|
|
<div className="relative">
|
|
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="uid={username},ou=people..."
|
|
value={ldapConfig.user_template}
|
|
onChange={(e) => setLdapConfig({...ldapConfig, user_template: e.target.value})}
|
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Groups Base DN</label>
|
|
<div className="relative">
|
|
<Layers className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-700" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="ou=groups"
|
|
value={ldapConfig.groups_dn}
|
|
onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})}
|
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-slate-950/40 border border-slate-800/60 rounded-2xl group transition-all hover:border-indigo-500/30">
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
"w-8 h-8 rounded-xl flex items-center justify-center transition-all shadow-inner",
|
|
ldapConfig.use_tls ? "bg-indigo-500/10 text-indigo-400" : "bg-slate-800 text-slate-500"
|
|
)}>
|
|
<Lock size={14} />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-black text-slate-200 uppercase tracking-widest leading-none">Use TLS</p>
|
|
<p className={cn("text-[8px] font-bold mt-1", ldapConfig.use_tls ? "text-indigo-400/80" : "text-slate-600")}>
|
|
{ldapConfig.use_tls ? "Encrypted Channel" : "Standard Link"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setLdapConfig({...ldapConfig, use_tls: !ldapConfig.use_tls})}
|
|
className={cn(
|
|
"w-10 h-5 rounded-full relative transition-all duration-300 shadow-inner border",
|
|
ldapConfig.use_tls ? "bg-indigo-600 border-indigo-500" : "bg-slate-800 border-slate-700"
|
|
)}
|
|
>
|
|
<div className={cn(
|
|
"absolute top-0.5 w-3.5 h-3.5 bg-white rounded-full transition-all shadow-sm",
|
|
ldapConfig.use_tls ? "right-0.5" : "left-0.5"
|
|
)} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<button
|
|
onClick={handleTestLdap}
|
|
disabled={testingLdap}
|
|
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl py-3 text-xs font-black shadow-xl shadow-indigo-600/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-indigo-500/30"
|
|
>
|
|
{testingLdap ? <RotateCcw size={14} className="animate-spin" /> : <Wifi size={14} />}
|
|
Test Connection
|
|
</button>
|
|
<button
|
|
onClick={handleUpdateLdap}
|
|
className="flex-[2] bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl py-3 text-xs font-black shadow-xl shadow-indigo-600/10 transition-all active:scale-95 flex items-center justify-center gap-2 border border-indigo-500/30 uppercase tracking-widest"
|
|
>
|
|
<Shield size={14} /> Save LDAP Policy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
</div>
|
|
|
|
{/* AI Intelligence Section - Full Width */}
|
|
<section className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ai">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<Brain size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">AI Intelligence</h2>
|
|
</div>
|
|
</div>
|
|
|
|
{/* [NEW] AI Provider Selection */}
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
{aiConfig?.providers?.map((p: any) => (
|
|
<button
|
|
key={p.id}
|
|
onClick={() => handleUpdateAiProvider(p.id)}
|
|
className={cn(
|
|
"p-4 rounded-2xl border transition-all text-left flex items-center justify-between group",
|
|
p.active
|
|
? "bg-indigo-600 border-indigo-500 shadow-xl shadow-indigo-600/10"
|
|
: "bg-slate-950/60 border-slate-800 hover:border-indigo-500/40"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
"w-8 h-8 rounded-lg flex items-center justify-center transition-colors",
|
|
p.active ? "bg-white/20 text-white" : "bg-indigo-500/10 text-indigo-400 group-hover:bg-indigo-500/20"
|
|
)}>
|
|
{p.id === 'gemini' ? <Cpu size={16} /> : <Zap size={16} />}
|
|
</div>
|
|
<div>
|
|
<p className={cn("text-xs font-black uppercase tracking-widest", p.active ? "text-white" : "text-slate-200")}>{p.name}</p>
|
|
<p className={cn(
|
|
"text-[9px] font-black mt-0.5 px-0.5 rounded uppercase tracking-tighter",
|
|
p.active
|
|
? (p.configured ? "text-emerald-200" : "text-rose-100")
|
|
: (p.configured ? "text-emerald-500" : "text-rose-500")
|
|
)}>
|
|
{p.configured ? "● Key Configured" : "○ Missing API Key"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{p.active && (
|
|
<div className="bg-white/20 px-2 py-0.5 rounded-full text-[8px] font-black text-white uppercase tracking-tighter">
|
|
Active
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* [NEW] AI Key Configuration */}
|
|
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-[2rem] p-6 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-400"><Lock size={14} /></div>
|
|
<h3 className="text-xs font-black text-slate-200 uppercase tracking-widest">Provider Access Keys</h3>
|
|
</div>
|
|
<button
|
|
onClick={handleSaveAiKeys}
|
|
disabled={isSavingKeys}
|
|
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 shadow-xl shadow-indigo-600/10 uppercase tracking-widest border border-indigo-500/30 flex items-center gap-2"
|
|
>
|
|
{isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />}
|
|
{isSavingKeys ? "Storing..." : "Store Keys"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Gemini API Key</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="password"
|
|
value={aiKeys.gemini}
|
|
onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})}
|
|
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."}
|
|
className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600"
|
|
/>
|
|
<button
|
|
onClick={() => handleTestAiKey('gemini')}
|
|
disabled={isTestingKeys.gemini}
|
|
className={cn(
|
|
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-tighter transition-all border shadow-lg flex items-center gap-1.5",
|
|
isTestingKeys.gemini
|
|
? "bg-slate-800 border-slate-700 text-slate-500 cursor-wait"
|
|
: "bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500"
|
|
)}
|
|
>
|
|
{isTestingKeys.gemini ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
|
{isTestingKeys.gemini ? "..." : "Test"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Claude API Key</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="password"
|
|
value={aiKeys.claude}
|
|
onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})}
|
|
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."}
|
|
className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600"
|
|
/>
|
|
<button
|
|
onClick={() => handleTestAiKey('claude')}
|
|
disabled={isTestingKeys.claude}
|
|
className={cn(
|
|
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-tighter transition-all border shadow-lg flex items-center gap-1.5",
|
|
isTestingKeys.claude
|
|
? "bg-slate-800 border-slate-700 text-slate-500 cursor-wait"
|
|
: "bg-indigo-600 border-indigo-500 text-white hover:bg-indigo-500"
|
|
)}
|
|
>
|
|
{isTestingKeys.claude ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
|
{isTestingKeys.claude ? "..." : "Test"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between px-1">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest">System Prompt (Vision Extraction)</label>
|
|
<button
|
|
onClick={handleUpdatePrompt}
|
|
disabled={isSavingPrompt}
|
|
className="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-[10px] font-black transition-all active:scale-95 shadow-xl shadow-indigo-600/10 uppercase tracking-widest border border-indigo-500/30 flex items-center gap-2"
|
|
>
|
|
{isSavingPrompt ? <RotateCcw size={14} className="animate-spin" /> : <FileText size={14} />}
|
|
{isSavingPrompt ? "Saving..." : "Save Prompt"}
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
value={aiPrompt}
|
|
onChange={(e) => setAiPrompt(e.target.value)}
|
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl p-6 text-[11px] font-mono font-bold text-slate-300 leading-relaxed outline-none focus:border-purple-500/50 transition-all min-h-[200px] custom-scrollbar shadow-inner"
|
|
/>
|
|
</div>
|
|
<div className="bg-purple-500/5 border border-purple-500/10 rounded-2xl p-4 flex gap-4 items-start">
|
|
<div className="p-2 bg-purple-500/10 rounded-lg text-purple-400 shrink-0"><Shield size={14} /></div>
|
|
<p className="text-[10px] font-bold text-slate-500 leading-relaxed">
|
|
This prompt instructs the Vision AI core on label interpretation. Ensure it defines explicit mapping for technical attributes like <span className="text-purple-400">Item</span>, <span className="text-purple-400">Type</span>, and <span className="text-purple-400">Part Number</span> to avoid extraction null-pointers and ensure inventory data integrity.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Categories Section */}
|
|
<section className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/categories">
|
|
<div className="flex items-center justify-between px-2">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<Layers size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">Category Groups</h2>
|
|
</div>
|
|
<button
|
|
onClick={handleAddCategory}
|
|
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-500 text-white px-6 py-3 rounded-xl text-xs font-black transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 uppercase tracking-widest"
|
|
>
|
|
<Plus size={14} /> New Group
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{categories.map(cat => (
|
|
<div key={cat.id} className="p-4 bg-slate-950/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
|
|
<div className="min-w-0 pr-4">
|
|
<p className="text-sm font-bold text-slate-200 group-hover:text-primary transition-colors truncate">{cat.name}</p>
|
|
<p className="text-[10px] text-slate-500 font-medium truncate mt-0.5">{cat.description || 'General storage'}</p>
|
|
</div>
|
|
<div className="flex gap-1 shrink-0">
|
|
<button
|
|
onClick={() => {
|
|
setEditingCategory(cat);
|
|
setEditCatForm({ name: cat.name, description: cat.description || '' });
|
|
}}
|
|
className="p-2 text-slate-600 hover:text-white hover:bg-slate-800 rounded-lg transition-all"
|
|
>
|
|
<Edit2 size={14} />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteCategory(cat.id, cat.name)}
|
|
className="p-2 text-slate-600 hover:text-red-500 hover:bg-red-500/5 rounded-lg transition-all"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Advanced Database - Bottom Sheet style for mobile */}
|
|
<section className="bg-slate-900/40 backdrop-blur-xl border border-slate-800/50 rounded-[2.5rem] overflow-hidden shadow-2xl transition-all group/storage">
|
|
<div className="p-5 md:p-8 space-y-8">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<HardDrive size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-black text-white tracking-tight">Storage & Recovery</h2>
|
|
</div>
|
|
<div className="flex items-center gap-2.5 w-full sm:w-auto">
|
|
<button
|
|
onClick={handleExportDb}
|
|
className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-8 py-3.5 bg-indigo-600 text-white hover:bg-indigo-500 rounded-xl text-[10px] font-black transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 uppercase tracking-widest"
|
|
>
|
|
<Download size={14} /> Export
|
|
</button>
|
|
<label className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-8 py-3.5 bg-indigo-600 text-white hover:bg-indigo-500 rounded-xl text-[10px] font-black cursor-pointer transition-all shadow-xl shadow-indigo-600/10 active:scale-95 border border-indigo-500/30 uppercase tracking-widest">
|
|
<Upload size={14} /> Import
|
|
<input type="file" accept=".db" className="hidden" onChange={handleImportDb} disabled={isImporting} />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-1 space-y-4">
|
|
<div className="p-5 bg-slate-950/60 border border-slate-800 rounded-2xl space-y-4">
|
|
<h3 className="text-[10px] font-black text-slate-500 uppercase tracking-widest flex items-center gap-2">
|
|
<Clock size={12} className="text-amber-500" /> System Policy
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[9px] font-black text-slate-600 px-1 uppercase tracking-tighter">Retention Limit</label>
|
|
<input
|
|
type="number"
|
|
value={dbSettings.retention_count}
|
|
onChange={(e) => handleUpdateDbSettings({...dbSettings, retention_count: parseInt(e.target.value) || 1})}
|
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-3 text-xs text-white font-mono outline-none focus:border-amber-500/30"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[9px] font-black text-slate-600 px-1 uppercase tracking-tighter">Backup Hour</label>
|
|
<select
|
|
value={dbSettings.schedule_hour}
|
|
onChange={(e) => handleUpdateDbSettings({...dbSettings, schedule_hour: parseInt(e.target.value)})}
|
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-3 text-xs text-white outline-none"
|
|
>
|
|
{Array.from({length: 24}).map((_, i) => <option key={i} value={i}>{String(i).padStart(2, '0')}:00</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[9px] font-black text-slate-600 px-1 uppercase tracking-tighter">Frequency</label>
|
|
<select
|
|
value={dbSettings.schedule_freq_days}
|
|
onChange={(e) => handleUpdateDbSettings({...dbSettings, schedule_freq_days: parseInt(e.target.value)})}
|
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 px-3 text-xs text-white outline-none font-black"
|
|
>
|
|
<option value={1}>Daily</option>
|
|
<option value={2}>Every 2 days</option>
|
|
<option value={3}>Every 3 days</option>
|
|
<option value={5}>Every 5 days</option>
|
|
<option value={7}>Weekly</option>
|
|
<option value={14}>Every 14 days</option>
|
|
<option value={30}>Monthly (30 days)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-2">
|
|
<div className="bg-slate-950/40 border border-slate-800/80 rounded-2xl overflow-hidden flex flex-col h-full min-h-[300px] shadow-inner">
|
|
<div className="px-5 py-3 border-b border-slate-800/50 bg-slate-900/40 flex items-center justify-between">
|
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Available Snapshots</span>
|
|
<span className="text-[10px] font-bold text-slate-600 tabular-nums">{backups.length} archived</span>
|
|
</div>
|
|
<div className="overflow-y-auto flex-1 divide-y divide-slate-900/50 custom-scrollbar">
|
|
{backups.map(b => (
|
|
<div key={b.filename} className="p-4 flex items-center justify-between hover:bg-slate-900/20 transition-all group">
|
|
<div className="flex items-center gap-4 min-w-0">
|
|
<div className="w-9 h-9 bg-slate-900 rounded-xl flex items-center justify-center text-slate-600 group-hover:text-amber-500 transition-colors border border-slate-800 shrink-0">
|
|
<History size={16} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-xs font-bold text-slate-200 truncate pr-4">{b.filename}</p>
|
|
<p className="text-[9px] font-black text-slate-600 tabular-nums mt-0.5">{new Date(b.created_at).toLocaleString()} · {formatSize(b.size_bytes)}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleRestore(b.filename)}
|
|
className="bg-indigo-500/10 hover:bg-indigo-600 text-indigo-400 hover:text-white px-4 py-1.5 rounded-lg text-[9px] font-black transition-all border border-indigo-500/20 hover:border-indigo-500 shrink-0 uppercase tracking-widest shadow-lg"
|
|
>
|
|
Restore
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{/* Shared Modals - Refined for Mobile Viewports */}
|
|
{(editingUser || editingCategory) && (
|
|
<div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-300">
|
|
<div className="w-full max-w-lg bg-slate-900 border-x border-t sm:border border-slate-800 rounded-t-[2.5rem] sm:rounded-3xl p-6 sm:p-10 shadow-2xl space-y-8 overflow-hidden animate-in slide-in-from-bottom-10">
|
|
{editingUser && (
|
|
<>
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20 text-indigo-400">
|
|
<User size={20} />
|
|
</div>
|
|
<h3 className="text-xl font-black text-white tracking-tight">Edit Profile</h3>
|
|
</div>
|
|
<button onClick={() => setEditingUser(null)} className="p-3 hover:bg-slate-800 rounded-2xl text-slate-500 transition-colors border border-slate-800">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-6">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Username</label>
|
|
<input
|
|
type="text"
|
|
disabled={editingUser.username === 'Admin'}
|
|
value={editUserForm.username}
|
|
onChange={(e) => setEditUserForm({...editUserForm, username: e.target.value})}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all disabled:opacity-40"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Update Password</label>
|
|
<input
|
|
type="password"
|
|
placeholder="••••••••••"
|
|
value={editUserForm.password}
|
|
onChange={(e) => setEditUserForm({...editUserForm, password: e.target.value})}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Account Power</label>
|
|
<select
|
|
value={editUserForm.role}
|
|
onChange={(e) => setEditUserForm({...editUserForm, role: e.target.value})}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none appearance-none font-black"
|
|
>
|
|
<option value="user">Standard Inventory User</option>
|
|
<option value="admin">System Administrator</option>
|
|
</select>
|
|
</div>
|
|
<button onClick={handleUpdateUserSubmit} className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black py-4 rounded-2xl shadow-xl shadow-indigo-600/20 active:scale-95 transition-all text-xs mb-4 uppercase tracking-widest">
|
|
Confirm Changes
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{editingCategory && (
|
|
<>
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
|
<Tag size={20} />
|
|
</div>
|
|
<h3 className="text-xl font-black text-white tracking-tight">Modify Group</h3>
|
|
</div>
|
|
<button onClick={() => setEditingCategory(null)} className="p-3 hover:bg-slate-800 rounded-2xl text-slate-500 transition-colors border border-slate-800">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-6">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Group Identifier</label>
|
|
<input
|
|
type="text"
|
|
value={editCatForm.name}
|
|
onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Strategic Description</label>
|
|
<textarea
|
|
value={editCatForm.description}
|
|
onChange={(e) => setEditCatForm({...editCatForm, description: e.target.value})}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-4 px-5 text-sm text-white focus:border-primary outline-none transition-all h-32 resize-none"
|
|
/>
|
|
</div>
|
|
<button onClick={handleUpdateCategorySubmit} className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black py-4 rounded-2xl shadow-xl shadow-indigo-600/20 active:scale-95 transition-all text-xs mb-4 uppercase tracking-widest">
|
|
Update Asset Group
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|