Files
tfm_ainventory/frontend/app/admin/page.tsx
2026-04-11 11:49:31 +03:00

688 lines
32 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { inventoryApi } from '@/lib/api';
import PageShell from '@/components/PageShell';
import {
Shield,
UserPlus,
User,
Trash2,
Tag,
Plus,
AlertTriangle,
LogOut,
Database,
History,
Lock,
Edit2,
X,
Server,
Globe,
Wifi,
WifiOff,
Layers,
ChevronDown
} 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: 'ldap://192.168.84.107:3890',
base_dn: 'dc=example,dc=com',
user_template: 'cn={username},ou=people,dc=example,dc=com',
required_group: 'inventory',
groups_dn: 'ou=groups',
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: '' });
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const [u, c, l] = await Promise.all([
inventoryApi.getUsers(),
inventoryApi.getCategories(),
inventoryApi.getLdapConfig()
]);
setUsers(u);
setCategories(c);
if (l && l.server_uri) setLdapConfig(l);
} catch (err) {
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) {
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) {
toast.error("Delete failed");
}
};
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) {
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) {
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) {
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) {
toast.error("Update failed");
}
};
const handleLogout = () => {
localStorage.removeItem('inventory_user');
window.location.href = '/';
};
return (
<PageShell requireAdmin={true}>
<main className="p-4 md:p-8 max-w-6xl mx-auto space-y-16">
<header className="flex items-center gap-6">
<div className="p-4 bg-primary/10 rounded-3xl text-primary border border-primary/20 shadow-xl shadow-primary/5">
<Shield size={40} />
</div>
<div>
<h1 className="text-4xl font-black tracking-tight text-white">System Admin</h1>
<p className="text-xs text-slate-500 font-bold mt-1">Enterprise Control Center</p>
</div>
</header>
{/* User Management Section */}
<section className="space-y-6">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-3">
<User size={20} className="text-primary" />
<h2 className="text-lg font-black text-white">User Accounts</h2>
</div>
<button
onClick={handleAddUser}
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-4 py-2 rounded-xl transition-all border border-primary/20 active:scale-95"
>
<UserPlus size={14} /> Add Local User
</button>
</div>
<div className="space-y-12">
{/* Local Users Group */}
<div className="space-y-4">
<div className="flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
<h3 className="text-sm font-black text-white">Local Users</h3>
</div>
</div>
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
{loading ? (
<div className="p-8 text-center animate-pulse text-slate-600 text-xs font-black">Loading...</div>
) : (
users.filter(u => (u.origin || 'local') === 'local').map(u => (
<div key={u.id} className="flex items-center justify-between p-4 hover:bg-slate-800/30 transition-all group">
<div className="flex items-center gap-4">
<div className={cn(
"p-2.5 rounded-xl",
u.role === 'admin' ? "bg-primary/10 text-primary border border-primary/20" : "bg-slate-800 text-slate-500 border border-slate-700"
)}>
{u.role === 'admin' ? <Shield size={16} /> : <User size={16} />}
</div>
<div>
<div className="flex items-center gap-2">
<p className="text-sm font-bold text-white group-hover:text-primary transition-colors">{u.username}</p>
<span className="text-[8px] font-black text-slate-500 opacity-60 bg-slate-800/50 px-1.5 py-0.5 rounded-md border border-slate-700/50 font-mono tracking-tighter">{u.role}</span>
</div>
</div>
</div>
<div className="flex items-center gap-1 transition-opacity pr-2">
<button
onClick={() => {
setEditingUser(u);
setEditUserForm({ username: u.username, password: '', role: u.role });
}}
title="Edit User"
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-700/50"
>
<Edit2 size={14} />
</button>
{u.username !== 'Admin' && (
<button
onClick={() => handleDeleteUser(u.id, u.username)}
title="Delete User"
className="p-2.5 bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white rounded-lg transition-all border border-rose-500/10"
>
<Trash2 size={14} />
</button>
)}
</div>
</div>
))
)}
</div>
</div>
{/* enterprise Users Group */}
{users.some(u => u.origin === 'ldap') && (
<div className="space-y-4">
<div className="flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
<h3 className="text-sm font-black text-white">Enterprise Users (LDAP)</h3>
</div>
</div>
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-3xl overflow-hidden shadow-xl divide-y divide-indigo-500/10">
{users.filter(u => u.origin === 'ldap').map(u => (
<div key={u.id} className="flex items-center justify-between p-4 hover:bg-indigo-500/10 transition-all group">
<div className="flex items-center gap-4">
<div className={cn(
"p-2.5 rounded-xl",
u.role === 'admin' ? "bg-indigo-500/20 text-indigo-400 border border-indigo-400/20" : "bg-slate-800/50 text-slate-500 border border-slate-800"
)}>
<Shield size={16} />
</div>
<div>
<div className="flex items-center gap-3">
<p className="text-sm font-bold text-white">{u.username}</p>
<span className="text-[8px] bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 px-1.5 py-0.5 rounded-md font-black">Network Sync</span>
<span className="text-[8px] font-black text-slate-500 opacity-60 bg-slate-800/50 px-1.5 py-0.5 rounded-md border border-slate-700/50 font-mono tracking-tighter">{u.role}</span>
</div>
</div>
</div>
<div className="text-xs font-black text-indigo-400/50 pr-4 truncate max-w-[150px] italic">
Read Only Profile
</div>
</div>
))}
</div>
</div>
)}
</div>
</section>
{/* Category Management Section */}
<section className="space-y-6">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-3">
<Layers size={20} className="text-primary" />
<h2 className="text-lg font-black text-white">Category Groups</h2>
</div>
<button
onClick={handleAddCategory}
className="flex items-center justify-center gap-2 bg-primary/10 hover:bg-primary text-primary hover:text-white font-black text-xs px-4 py-2 rounded-xl transition-all border border-primary/20 active:scale-95"
>
<Plus size={14} /> Add New Group
</button>
</div>
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
{loading ? (
<div className="p-8 text-center animate-pulse text-slate-600 text-xs font-black">Loading Categories...</div>
) : (
categories.map(cat => (
<div key={cat.id} className="flex items-center justify-between p-4 hover:bg-slate-800/30 transition-all group">
<div className="flex items-center gap-4 min-w-0 flex-1">
<div className="p-2.5 bg-slate-800 text-slate-500 rounded-xl border border-slate-700 shadow-inner group-hover:text-primary group-hover:bg-primary/5 transition-all shrink-0">
<Layers size={16} />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-white group-hover:text-primary transition-colors truncate">{cat.name}</p>
<p className="text-xs text-slate-500 font-medium truncate opacity-60">
{cat.description || 'General Purpose Group'}
</p>
</div>
</div>
<div className="flex items-center gap-1 transition-opacity ml-4 pr-2">
<button
onClick={() => {
setEditingCategory(cat);
setEditCatForm({ name: cat.name, description: cat.description || '' });
}}
title="Edit Category"
className="p-2.5 bg-slate-800/50 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-all border border-slate-700/50"
>
<Edit2 size={14} />
</button>
<button
onClick={() => handleDeleteCategory(cat.id, cat.name)}
title="Delete Category"
className="p-2.5 bg-rose-500/5 hover:bg-rose-500 text-rose-500 hover:text-white rounded-lg transition-all border border-rose-500/10"
>
<Trash2 size={14} />
</button>
</div>
</div>
))
)}
{categories.length === 0 && !loading && (
<div className="p-8 text-center text-slate-600 text-xs font-black italic">
No categories defined
</div>
)}
</div>
</section>
{/* System Settings Section */}
<div className="pt-8 border-t border-slate-900">
<section className="p-8 bg-slate-900/50 rounded-[3rem] border border-slate-800 flex flex-col items-center text-center gap-6 shadow-inner">
<div className="w-16 h-16 bg-primary/10 rounded-3xl flex items-center justify-center text-primary border border-primary/20 shadow-lg shadow-primary/5">
<Database size={32} />
</div>
<div>
<h3 className="text-xl font-black text-white tracking-tight">System Integrity</h3>
<p className="text-xs text-slate-500 mt-3 max-w-[280px] mx-auto leading-relaxed">
Hybrid storage model active. Real-time synchronization between local memory and cloud-hosted SQLite.
</p>
</div>
<div className="flex items-center gap-3 bg-slate-950 px-6 py-2.5 rounded-full border border-slate-800 shadow-2xl">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.5)]" />
<span className="text-xs font-black text-green-500">Storage Online</span>
</div>
</section>
</div>
{/* Enterprise LDAP Integration */}
<section className="space-y-8 bg-slate-900/30 border border-slate-800/40 p-8 rounded-[3rem] shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
<Globe size={120} />
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 px-2 relative z-10">
<div className="flex items-center gap-4">
<div className="p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20">
<Server size={24} />
</div>
<div>
<h2 className="text-xl font-black text-white">Enterprise Integration</h2>
<p className="text-xs text-slate-500 font-bold mt-1">LLDAP / Active Directory Connectivity</p>
</div>
</div>
<div className="flex items-center gap-3 bg-slate-950 p-2 rounded-2xl border border-slate-800">
<span className="text-xs font-black text-slate-400 px-3">LDAP Status</span>
<button
onClick={() => {
const newConfig = { ...ldapConfig, ldap_enabled: !ldapConfig.ldap_enabled };
setLdapConfig(newConfig);
inventoryApi.updateLdapConfig(newConfig);
toast.success(`LDAP ${newConfig.ldap_enabled ? 'Enabled' : 'Disabled'}`);
}}
className={cn(
"px-6 py-2 rounded-xl text-xs font-black transition-all",
ldapConfig.ldap_enabled ? "bg-green-500/10 text-green-500 border border-green-500/20" : "bg-slate-800 text-slate-500"
)}
>
{ldapConfig.ldap_enabled ? 'Active' : 'Offline'}
</button>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-8 relative z-10">
<div className="space-y-6">
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">Server URI</label>
<div className="relative group/input">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-600 group-focus-within/input:text-indigo-400 transition-colors">
<Wifi size={16} />
</div>
<input
type="text"
value={ldapConfig.server_uri || ''}
onChange={(e) => setLdapConfig({ ...ldapConfig, server_uri: e.target.value })}
placeholder="ldap://192.168.84.107:3890"
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 pl-12 pr-5 text-white outline-none transition-all font-mono text-sm"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">Base DN (Suffix)</label>
<input
type="text"
value={ldapConfig.base_dn || ''}
onChange={(e) => setLdapConfig({ ...ldapConfig, base_dn: e.target.value })}
placeholder="dc=example,dc=com"
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 px-5 text-white outline-none transition-all font-mono text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">User DN Template</label>
<input
type="text"
value={ldapConfig.user_template || ''}
onChange={(e) => setLdapConfig({ ...ldapConfig, user_template: e.target.value })}
placeholder="uid={username},ou=people,dc=..."
className="w-full bg-slate-950 border border-slate-800 focus:border-indigo-500/50 rounded-2xl py-4 px-5 text-white outline-none transition-all font-mono text-sm"
/>
</div>
</div>
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between px-1">
<label className="text-xs font-black text-slate-500">Role Mappings</label>
<button
onClick={() => {
const newMappings = [...(ldapConfig.role_mappings || []), { group: '', role: 'user' }];
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
}}
className="text-xs font-black text-indigo-400 hover:text-indigo-300 transition-colors"
>
+ Add Mapping
</button>
</div>
<div className="space-y-2 max-h-[180px] overflow-y-auto pr-2 custom-scrollbar">
{(ldapConfig.role_mappings || []).map((mapping: any, idx: number) => (
<div key={idx} className="flex gap-2 items-center bg-slate-950/50 p-3 rounded-2xl border border-slate-800/50">
<input
type="text"
value={mapping.group}
onChange={(e) => {
const newMappings = [...ldapConfig.role_mappings];
newMappings[idx].group = e.target.value;
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
}}
placeholder="Group CN (e.g. admins)"
className="flex-1 bg-transparent text-xs font-mono text-white outline-none"
/>
<select
value={mapping.role}
onChange={(e) => {
const newMappings = [...ldapConfig.role_mappings];
newMappings[idx].role = e.target.value;
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
}}
className="bg-slate-900 text-xs font-bold text-slate-400 px-2 py-1 rounded-lg border border-slate-700 outline-none"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button
onClick={() => {
const newMappings = ldapConfig.role_mappings.filter((_: any, i: number) => i !== idx);
setLdapConfig({ ...ldapConfig, role_mappings: newMappings });
}}
className="text-slate-600 hover:text-red-400 p-1"
>
<X size={14} />
</button>
</div>
))}
{(ldapConfig.role_mappings || []).length === 0 && (
<div className="text-center py-4 border border-dashed border-slate-800 rounded-2xl text-xs text-slate-600">
No roles mapped
</div>
)}
</div>
</div>
<div className="p-6 bg-slate-950 rounded-3xl border border-slate-800 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Lock size={16} className="text-slate-500" />
<span className="text-sm font-bold text-slate-300">Encrypted Connection (TLS)</span>
</div>
<button
onClick={() => setLdapConfig({ ...ldapConfig, use_tls: !ldapConfig.use_tls })}
className={cn(
"w-12 h-6 rounded-full relative transition-all",
ldapConfig.use_tls ? "bg-indigo-500" : "bg-slate-800"
)}
>
<div className={cn(
"absolute top-1 w-4 h-4 bg-white rounded-full transition-all",
ldapConfig.use_tls ? "right-1" : "left-1"
)} />
</button>
</div>
<div className="pt-4 border-t border-slate-900 flex gap-4">
<button
onClick={async () => {
setTestingLdap(true);
try {
const res = await inventoryApi.testLdapConnection(ldapConfig);
if (res.status === 'success') {
toast.success("Connection Successful!");
} else {
toast.error(res.message);
}
} catch (err) {
toast.error("Network Error during test");
} finally {
setTestingLdap(false);
}
}}
disabled={testingLdap}
className="flex-1 bg-slate-900 hover:bg-slate-800 text-xs font-black py-4 rounded-xl border border-slate-800 transition-all flex items-center justify-center gap-2"
>
{testingLdap ? <div className="w-4 h-4 border-2 border-indigo-400 border-t-transparent animate-spin rounded-full" /> : <Wifi size={14} />}
Test Link
</button>
<button
onClick={async () => {
await inventoryApi.updateLdapConfig(ldapConfig);
toast.success("Settings applied");
}}
className="flex-1 bg-indigo-600 hover:bg-indigo-500 shadow-lg shadow-indigo-900/20 text-white text-xs font-black py-4 rounded-xl transition-all"
>
Save Changes
</button>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-indigo-500/5 rounded-2xl border border-indigo-500/10">
<AlertTriangle size={16} className="text-indigo-400 shrink-0 mt-0.5" />
<p className="text-xs text-indigo-300/60 leading-normal italic">
LLDAP usually serves LDAP on port 3890. Ensure your firewall allows traffic on this port between the inventory server and 192.168.84.107.
</p>
</div>
</div>
</div>
</section>
{/* Edit User Modal */}
{editingUser && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-300">
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-8 max-w-md w-full shadow-2xl space-y-8">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<div className="p-3 bg-primary/10 rounded-xl text-primary border border-primary/20">
<User size={20} />
</div>
<h3 className="text-xl font-black text-white tracking-tight">Edit Profile</h3>
</div>
<button onClick={() => setEditingUser(null)} className="p-2 hover:bg-slate-800 rounded-lg text-slate-500 transition-colors">
<X size={20} />
</button>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-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 focus:border-primary rounded-2xl py-4 px-5 text-white outline-none transition-all disabled:opacity-50"
/>
{editingUser.username === 'Admin' && <p className="text-xs text-amber-500/60 font-bold px-1 italic">Default Admin name is protected</p>}
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">New Password (leave empty to keep current)</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 focus:border-primary rounded-2xl py-4 px-5 text-white/50 focus:text-white outline-none transition-all placeholder:text-slate-800"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">Access Role</label>
<div className="relative flex items-center">
<select
value={editUserForm.role}
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
className="w-full bg-slate-950 border border-slate-800 focus:border-primary rounded-2xl py-4 px-5 text-white outline-none transition-all appearance-none"
>
<option value="user">USER (Standard)</option>
<option value="admin">ADMIN (Root Access)</option>
</select>
<ChevronDown size={20} className="absolute right-5 text-slate-500 pointer-events-none" />
</div>
</div>
<button
onClick={handleUpdateUserSubmit}
className="w-full bg-primary text-white font-black py-5 rounded-2xl shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all"
>
Save Profile Changes
</button>
</div>
</div>
</div>
)}
{/* Edit Category Modal */}
{editingCategory && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-300">
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-8 max-w-md w-full shadow-2xl space-y-8">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<div className="p-3 bg-primary/10 rounded-xl text-primary border border-primary/20">
<Tag size={20} />
</div>
<h3 className="text-xl font-black text-white tracking-tight">Edit Group</h3>
</div>
<button onClick={() => setEditingCategory(null)} className="p-2 hover:bg-slate-800 rounded-lg text-slate-500 transition-colors">
<X size={20} />
</button>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">Group Name</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 focus:border-primary rounded-2xl py-4 px-5 text-white outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 px-1">Description</label>
<textarea
value={editCatForm.description}
onChange={(e) => setEditCatForm({ ...editCatForm, description: e.target.value })}
className="w-full bg-slate-950 border border-slate-800 focus:border-primary rounded-2xl py-4 px-5 text-white outline-none transition-all h-32 resize-none"
/>
</div>
<button
onClick={handleUpdateCategorySubmit}
className="w-full bg-primary text-white font-black py-5 rounded-2xl shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all"
>
Save Group Changes
</button>
</div>
</div>
</div>
)}
</main>
</PageShell>
);
}