336 lines
13 KiB
TypeScript
336 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useLanguage } from '@/context/LanguageContext';
|
|
import { usersApi, User } from '@/lib/api';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import Input from '@/components/ui/Input';
|
|
import { TrashIcon, PencilSquareIcon } from '@heroicons/react/24/outline';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function AdminUsersPage() {
|
|
const { t, locale } = useLanguage();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [roleFilter, setRoleFilter] = useState<string>('');
|
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
const [editForm, setEditForm] = useState({
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
role: '' as User['role'],
|
|
languagePreference: '' as string,
|
|
accountStatus: '' as string,
|
|
});
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadUsers();
|
|
}, [roleFilter]);
|
|
|
|
const loadUsers = async () => {
|
|
try {
|
|
const { users } = await usersApi.getAll(roleFilter || undefined);
|
|
setUsers(users);
|
|
} catch (error) {
|
|
toast.error('Failed to load users');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRoleChange = async (userId: string, newRole: string) => {
|
|
try {
|
|
await usersApi.update(userId, { role: newRole as any });
|
|
toast.success('Role updated');
|
|
loadUsers();
|
|
} catch (error) {
|
|
toast.error('Failed to update role');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (userId: string) => {
|
|
if (!confirm('Are you sure you want to delete this user?')) return;
|
|
|
|
try {
|
|
await usersApi.delete(userId);
|
|
toast.success('User deleted');
|
|
loadUsers();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to delete user');
|
|
}
|
|
};
|
|
|
|
const openEditModal = (user: User) => {
|
|
setEditingUser(user);
|
|
setEditForm({
|
|
name: user.name,
|
|
email: user.email,
|
|
phone: user.phone || '',
|
|
role: user.role,
|
|
languagePreference: user.languagePreference || '',
|
|
accountStatus: user.accountStatus || 'active',
|
|
});
|
|
};
|
|
|
|
const handleEditSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!editingUser) return;
|
|
|
|
if (!editForm.name.trim() || editForm.name.trim().length < 2) {
|
|
toast.error('Name must be at least 2 characters');
|
|
return;
|
|
}
|
|
if (!editForm.email.trim()) {
|
|
toast.error('Email is required');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
await usersApi.update(editingUser.id, {
|
|
name: editForm.name.trim(),
|
|
email: editForm.email.trim(),
|
|
phone: editForm.phone.trim() || undefined,
|
|
role: editForm.role,
|
|
languagePreference: editForm.languagePreference || undefined,
|
|
accountStatus: editForm.accountStatus || undefined,
|
|
} as Partial<User>);
|
|
toast.success('User updated successfully');
|
|
setEditingUser(null);
|
|
loadUsers();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to update user');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
timeZone: 'America/Asuncion',
|
|
});
|
|
};
|
|
|
|
const getRoleBadge = (role: string) => {
|
|
const styles: Record<string, string> = {
|
|
admin: 'badge-danger',
|
|
organizer: 'badge-info',
|
|
staff: 'badge-warning',
|
|
marketing: 'badge-success',
|
|
user: 'badge-gray',
|
|
};
|
|
return <span className={`badge ${styles[role] || 'badge-gray'}`}>{t(`admin.users.roles.${role}`)}</span>;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold text-primary-dark">{t('admin.users.title')}</h1>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card className="p-4 mb-6">
|
|
<div className="flex flex-wrap gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">{t('admin.users.role')}</label>
|
|
<select
|
|
value={roleFilter}
|
|
onChange={(e) => setRoleFilter(e.target.value)}
|
|
className="px-4 py-2 rounded-btn border border-secondary-light-gray min-w-[150px]"
|
|
>
|
|
<option value="">All Roles</option>
|
|
<option value="admin">{t('admin.users.roles.admin')}</option>
|
|
<option value="organizer">{t('admin.users.roles.organizer')}</option>
|
|
<option value="staff">{t('admin.users.roles.staff')}</option>
|
|
<option value="marketing">{t('admin.users.roles.marketing')}</option>
|
|
<option value="user">{t('admin.users.roles.user')}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Users Table */}
|
|
<Card className="overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-secondary-gray">
|
|
<tr>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">User</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Contact</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Role</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Joined</th>
|
|
<th className="text-right px-6 py-3 text-sm font-medium text-gray-600">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-secondary-light-gray">
|
|
{users.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-6 py-12 text-center text-gray-500">
|
|
No users found
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
users.map((user) => (
|
|
<tr key={user.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-primary-yellow/20 rounded-full flex items-center justify-center">
|
|
<span className="font-semibold text-primary-dark">
|
|
{user.name.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{user.name}</p>
|
|
<p className="text-sm text-gray-500">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-600">
|
|
{user.phone || '-'}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<select
|
|
value={user.role}
|
|
onChange={(e) => handleRoleChange(user.id, e.target.value)}
|
|
className="px-2 py-1 rounded border border-secondary-light-gray text-sm"
|
|
>
|
|
<option value="user">{t('admin.users.roles.user')}</option>
|
|
<option value="staff">{t('admin.users.roles.staff')}</option>
|
|
<option value="marketing">{t('admin.users.roles.marketing')}</option>
|
|
<option value="organizer">{t('admin.users.roles.organizer')}</option>
|
|
<option value="admin">{t('admin.users.roles.admin')}</option>
|
|
</select>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-600">
|
|
{formatDate(user.createdAt)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => openEditModal(user)}
|
|
className="p-2 hover:bg-blue-100 text-blue-600 rounded-btn"
|
|
title="Edit"
|
|
>
|
|
<PencilSquareIcon className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(user.id)}
|
|
className="p-2 hover:bg-red-100 text-red-600 rounded-btn"
|
|
title="Delete"
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Edit User Modal */}
|
|
{editingUser && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
|
<Card className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6">
|
|
<h2 className="text-xl font-bold text-primary-dark mb-6">Edit User</h2>
|
|
|
|
<form onSubmit={handleEditSubmit} className="space-y-4">
|
|
<Input
|
|
label="Name"
|
|
value={editForm.name}
|
|
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|
required
|
|
minLength={2}
|
|
/>
|
|
|
|
<Input
|
|
label="Email"
|
|
type="email"
|
|
value={editForm.email}
|
|
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
label="Phone"
|
|
value={editForm.phone}
|
|
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
|
|
placeholder="Optional"
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-primary-dark mb-1.5">Role</label>
|
|
<select
|
|
value={editForm.role}
|
|
onChange={(e) => setEditForm({ ...editForm, role: e.target.value as User['role'] })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:border-transparent"
|
|
>
|
|
<option value="user">{t('admin.users.roles.user')}</option>
|
|
<option value="staff">{t('admin.users.roles.staff')}</option>
|
|
<option value="marketing">{t('admin.users.roles.marketing')}</option>
|
|
<option value="organizer">{t('admin.users.roles.organizer')}</option>
|
|
<option value="admin">{t('admin.users.roles.admin')}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-primary-dark mb-1.5">Language Preference</label>
|
|
<select
|
|
value={editForm.languagePreference}
|
|
onChange={(e) => setEditForm({ ...editForm, languagePreference: e.target.value })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:border-transparent"
|
|
>
|
|
<option value="">Not set</option>
|
|
<option value="en">English</option>
|
|
<option value="es">Español</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-primary-dark mb-1.5">Account Status</label>
|
|
<select
|
|
value={editForm.accountStatus}
|
|
onChange={(e) => setEditForm({ ...editForm, accountStatus: e.target.value })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:border-transparent"
|
|
>
|
|
<option value="active">Active</option>
|
|
<option value="unclaimed">Unclaimed</option>
|
|
<option value="suspended">Suspended</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex gap-4 justify-end mt-6 pt-4 border-t border-secondary-light-gray">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setEditingUser(null)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" isLoading={saving}>
|
|
Save Changes
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|