Files
Spanglish/frontend/src/app/admin/users/page.tsx
T
MichilisandClaude Fable 5 9b2668f498 Unify admin ticket creation into one modal with first-class payment status.
- Replace the three Attendees-tab modals (Manual Ticket / Add at Door /
  Invite Guest) with a single Add Ticket modal: Paid/Unpaid/Guest segmented
  control, shared fields, "Check in now" for all types, and a live
  "what happens" preview, backed by one POST /api/tickets/admin/add.
- Add tickets.payment_status (paid | unpaid | comp) with a backfill
  migration; keep it in sync on every payment-settlement path (mark-paid,
  admin approval, Lightning, free bookings, hold recovery).
- Show Paid/Unpaid/Comp badges in the attendee list, count only paid
  tickets toward revenue, let unpaid tickets be resolved via Mark Paid,
  and flag unpaid tickets with their balance due in the door scanner.
- Replace the per-page useStatsPrivacy hook with an admin-wide
  PrivacyContext + SensitiveValue mask, toggled from the admin layout.
- Add server-side pagination with page-size options to the users page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 05:25:00 +00:00

607 lines
29 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { useLanguage } from '@/context/LanguageContext';
import { usersApi, eventsApi, User, Event } from '@/lib/api';
import { parseDate, formatRucDisplay } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { AdminPageSkeleton } from '@/components/ui/Skeleton';
import Input from '@/components/ui/Input';
import { MoreMenu, DropdownItem, BottomSheet, AdminMobileStyles } from '@/components/admin/MobileComponents';
import { TrashIcon, PencilSquareIcon, FunnelIcon, XMarkIcon, MagnifyingGlassIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
type RegisteredRange = '' | '7d' | '30d' | '90d';
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
function getPageNumbers(current: number, totalPages: number): (number | '...')[] {
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
const pages: (number | '...')[] = [1];
const start = Math.max(2, current - 1);
const end = Math.min(totalPages - 1, current + 1);
if (start > 2) pages.push('...');
for (let i = start; i <= end; i++) pages.push(i);
if (end < totalPages - 1) pages.push('...');
pages.push(totalPages);
return pages;
}
function registeredAfterFromRange(range: RegisteredRange): string | undefined {
if (!range) return undefined;
const days = range === '7d' ? 7 : range === '30d' ? 30 : 90;
const date = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
return date.toISOString();
}
export default function AdminUsersPage() {
const { t, locale } = useLanguage();
const [users, setUsers] = useState<User[]>([]);
const [total, setTotal] = useState(0);
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [roleFilter, setRoleFilter] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [hasBookingsFilter, setHasBookingsFilter] = useState<'' | 'yes' | 'no'>('');
const [registeredRange, setRegisteredRange] = useState<RegisteredRange>('');
const [eventFilter, setEventFilter] = useState<string>('');
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
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);
const [mobileFilterOpen, setMobileFilterOpen] = useState(false);
// Debounce the search box like the Emails page does (300ms).
useEffect(() => {
const handle = setTimeout(() => setDebouncedSearch(searchQuery), 300);
return () => clearTimeout(handle);
}, [searchQuery]);
useEffect(() => {
eventsApi.getAll().then((res) => setEvents(res.events)).catch(() => {});
}, []);
// When a filter changes, jump back to page 1 before fetching (skipping the
// fetch for the stale page); otherwise fetch for the current page/pageSize.
const filterKey = JSON.stringify([roleFilter, statusFilter, hasBookingsFilter, registeredRange, eventFilter, debouncedSearch]);
const prevFilterKey = useRef(filterKey);
useEffect(() => {
if (prevFilterKey.current !== filterKey) {
prevFilterKey.current = filterKey;
if (page !== 1) {
setPage(1);
return;
}
}
loadUsers();
}, [filterKey, page, pageSize]);
const hasActiveFilters =
roleFilter || statusFilter || hasBookingsFilter || registeredRange || eventFilter || searchQuery;
const clearFilters = () => {
setRoleFilter('');
setStatusFilter('');
setHasBookingsFilter('');
setRegisteredRange('');
setEventFilter('');
setSearchQuery('');
};
const loadUsers = async () => {
try {
const { users, total } = await usersApi.getAll({
role: roleFilter || undefined,
accountStatus: statusFilter || undefined,
hasBookings: hasBookingsFilter || undefined,
registeredAfter: registeredAfterFromRange(registeredRange),
eventId: eventFilter || undefined,
search: debouncedSearch.trim() || undefined,
page,
pageSize,
});
setUsers(users);
setTotal(total);
// If the current page emptied out (e.g. after deleting the last user on
// it), fall back to the new last page.
if (users.length === 0 && total > 0 && page > 1) {
setPage(Math.max(1, Math.ceil(total / pageSize)));
}
} 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 parseDate(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 <AdminPageSkeleton cols={6} />;
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-xl md:text-2xl font-bold text-primary-dark">{t('admin.users.title')} ({total})</h1>
</div>
{/* Desktop Filters */}
<Card className="p-4 mb-6 hidden md:block">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Search</label>
<div className="relative">
<MagnifyingGlassIcon className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Name, email, phone..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 rounded-btn border border-secondary-light-gray text-sm focus:outline-none focus:ring-2 focus:ring-primary-yellow"
/>
</div>
</div>
<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="w-full px-4 py-2 rounded-btn border border-secondary-light-gray text-sm">
<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>
<label className="block text-sm font-medium mb-1">Status</label>
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray text-sm">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="unclaimed">Unclaimed</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Has Bookings</label>
<select value={hasBookingsFilter} onChange={(e) => setHasBookingsFilter(e.target.value as '' | 'yes' | 'no')}
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray text-sm">
<option value="">All</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Registered</label>
<select value={registeredRange} onChange={(e) => setRegisteredRange(e.target.value as RegisteredRange)}
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray text-sm">
<option value="">All Time</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Event</label>
<select value={eventFilter} onChange={(e) => setEventFilter(e.target.value)}
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray text-sm">
<option value="">All Events</option>
{events.map((event) => (
<option key={event.id} value={event.id}>{event.title}</option>
))}
</select>
</div>
</div>
{hasActiveFilters && (
<div className="mt-3 text-xs text-gray-500 flex items-center gap-2">
<span>Showing {users.length} of {total}</span>
<button onClick={clearFilters} className="text-primary-yellow hover:underline">Clear</button>
</div>
)}
</Card>
{/* Mobile Toolbar */}
<div className="md:hidden mb-4 space-y-2">
<div className="relative">
<MagnifyingGlassIcon className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Name, email, phone..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
/>
</div>
<div className="flex items-center gap-2">
<button onClick={() => setMobileFilterOpen(true)}
className={clsx(
'flex items-center gap-1.5 px-3 py-2 rounded-btn border text-sm min-h-[44px]',
hasActiveFilters ? 'border-primary-yellow bg-yellow-50 text-primary-dark' : 'border-secondary-light-gray text-gray-600'
)}>
<FunnelIcon className="w-4 h-4" />
Filters
</button>
{hasActiveFilters && (
<button onClick={clearFilters} className="text-xs text-primary-yellow min-h-[44px] flex items-center">
Clear
</button>
)}
<span className="text-xs text-gray-500 ml-auto">{total} users</span>
</div>
</div>
{/* Desktop: Table */}
<Card className="overflow-hidden hidden md:block">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-secondary-gray">
<tr>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Contact</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">RUC</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>
<th className="text-right px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-secondary-light-gray">
{users.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-12 text-center text-gray-500 text-sm">No users found</td></tr>
) : (
users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary-yellow/20 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-semibold text-sm text-primary-dark">{user.name.charAt(0).toUpperCase()}</span>
</div>
<div>
<p className="font-medium text-sm">{user.name}</p>
<p className="text-xs text-gray-500">{user.email}</p>
</div>
</div>
</td>
<td className="px-4 py-3 text-sm text-gray-600">{user.phone || '-'}</td>
<td className="px-4 py-3 text-sm text-gray-600">{formatRucDisplay(user.rucNumber) || '-'}</td>
<td className="px-4 py-3">
<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-4 py-3 text-xs text-gray-500">{formatDate(user.createdAt)}</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-1">
<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>
{/* Mobile: Card List */}
<div className="md:hidden space-y-2">
{users.length === 0 ? (
<div className="text-center py-10 text-gray-500 text-sm">No users found</div>
) : (
users.map((user) => (
<Card key={user.id} className="p-3">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-primary-yellow/20 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-semibold text-primary-dark">{user.name.charAt(0).toUpperCase()}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="font-medium text-sm truncate">{user.name}</p>
<p className="text-xs text-gray-500 truncate">{user.email}</p>
{user.phone && <p className="text-[10px] text-gray-400">{user.phone}</p>}
{user.rucNumber && <p className="text-[10px] text-gray-400">RUC: {formatRucDisplay(user.rucNumber)}</p>}
</div>
{getRoleBadge(user.role)}
</div>
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
<p className="text-[10px] text-gray-400">Joined {formatDate(user.createdAt)}</p>
<MoreMenu>
<DropdownItem onClick={() => openEditModal(user)}>
<PencilSquareIcon className="w-4 h-4 mr-2" /> Edit User
</DropdownItem>
<DropdownItem onClick={() => handleDelete(user.id)} className="text-red-600">
<TrashIcon className="w-4 h-4 mr-2" /> Delete
</DropdownItem>
</MoreMenu>
</div>
</div>
</div>
</Card>
))
)}
</div>
{/* Pagination */}
{total > 0 && (
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm text-gray-600">
<label htmlFor="users-page-size" className="whitespace-nowrap">Per page</label>
<select
id="users-page-size"
value={pageSize}
onChange={(e) => { setPageSize(Number(e.target.value)); setPage(1); }}
className="px-2 py-1.5 rounded-btn border border-secondary-light-gray text-sm"
>
{PAGE_SIZE_OPTIONS.map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
<span className="text-xs text-gray-500 whitespace-nowrap">
{(page - 1) * pageSize + 1}&ndash;{Math.min(page * pageSize, total)} of {total}
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(page - 1)}
disabled={page <= 1}
className="p-2 rounded-btn border border-secondary-light-gray text-gray-600 hover:bg-gray-50 disabled:opacity-40 disabled:pointer-events-none min-h-[36px] min-w-[36px] flex items-center justify-center"
aria-label="Previous page"
>
<ChevronLeftIcon className="w-4 h-4" />
</button>
{getPageNumbers(page, Math.max(1, Math.ceil(total / pageSize))).map((p, i) =>
p === '...' ? (
<span key={`ellipsis-${i}`} className="px-1.5 text-sm text-gray-400">&hellip;</span>
) : (
<button
key={p}
onClick={() => setPage(p)}
className={clsx(
'min-h-[36px] min-w-[36px] px-2 rounded-btn text-sm',
p === page
? 'bg-primary-yellow text-primary-dark font-semibold'
: 'border border-secondary-light-gray text-gray-600 hover:bg-gray-50'
)}
aria-current={p === page ? 'page' : undefined}
>
{p}
</button>
)
)}
<button
onClick={() => setPage(page + 1)}
disabled={page >= Math.ceil(total / pageSize)}
className="p-2 rounded-btn border border-secondary-light-gray text-gray-600 hover:bg-gray-50 disabled:opacity-40 disabled:pointer-events-none min-h-[36px] min-w-[36px] flex items-center justify-center"
aria-label="Next page"
>
<ChevronRightIcon className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* Mobile Filter BottomSheet */}
<BottomSheet open={mobileFilterOpen} onClose={() => setMobileFilterOpen(false)} title="Filters">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('admin.users.role')}</label>
<select value={roleFilter} onChange={(e) => setRoleFilter(e.target.value)}
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
<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>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="unclaimed">Unclaimed</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Has Bookings</label>
<select value={hasBookingsFilter} onChange={(e) => setHasBookingsFilter(e.target.value as '' | 'yes' | 'no')}
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
<option value="">All</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Registered</label>
<select value={registeredRange} onChange={(e) => setRegisteredRange(e.target.value as RegisteredRange)}
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
<option value="">All Time</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Event</label>
<select value={eventFilter} onChange={(e) => setEventFilter(e.target.value)}
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
<option value="">All Events</option>
{events.map((event) => (
<option key={event.id} value={event.id}>{event.title}</option>
))}
</select>
</div>
<div className="flex gap-3 pt-2">
<Button variant="outline" onClick={() => { clearFilters(); setMobileFilterOpen(false); }} className="flex-1 min-h-[44px]">
Clear All
</Button>
<Button onClick={() => setMobileFilterOpen(false)} className="flex-1 min-h-[44px]">
Apply
</Button>
</div>
</div>
</BottomSheet>
{/* Edit User Modal */}
{editingUser && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<Card className="w-full md:max-w-lg max-h-[90vh] flex flex-col overflow-hidden rounded-t-2xl md:rounded-card">
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
<h2 className="text-base font-bold">Edit User</h2>
<button onClick={() => setEditingUser(null)}
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
<XMarkIcon className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleEditSubmit} className="p-4 space-y-4 overflow-y-auto flex-1 min-h-0">
<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 min-h-[44px]">
<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 min-h-[44px]">
<option value="">Not set</option>
<option value="en">English</option>
<option value="es">Espa&#241;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 min-h-[44px]">
<option value="active">Active</option>
<option value="unclaimed">Unclaimed</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div className="flex gap-3 pt-2">
<Button type="button" variant="outline" onClick={() => setEditingUser(null)} className="flex-1 min-h-[44px]">Cancel</Button>
<Button type="submit" isLoading={saving} className="flex-1 min-h-[44px]">Save Changes</Button>
</div>
</form>
</Card>
</div>
)}
<AdminMobileStyles />
</div>
);
}