码桶

发现社区成员的开源项目

main
UserManagement.jsx39.8 KB
import React, { useState, useEffect } from 'react';
import { User, Plus, Trash2, Edit2, RefreshCw, Save, Copy, CheckCircle, X } from 'lucide-react';

const UserManagement = ({ show, onClose, auth, t, showToast }) => {
    const [tab, setTab] = useState('users');
    const [userList, setUserList] = useState([]);
    const [userListLoading, setUserListLoading] = useState(false);
    const [showAddUser, setShowAddUser] = useState(false);
    const [newUserName, setNewUserName] = useState('');
    const [newUserRole, setNewUserRole] = useState('user');
    const [addUserLoading, setAddUserLoading] = useState(false);
    const [addUserError, setAddUserError] = useState('');
    const [createdSetupToken, setCreatedSetupToken] = useState('');
    const [editingUser, setEditingUser] = useState(null);
    const [editUserRole, setEditUserRole] = useState('user');
    const [editUserLoading, setEditUserLoading] = useState(false);
    const [editAllowedZones, setEditAllowedZones] = useState([]);
    const [zoneInput, setZoneInput] = useState('');
    const [auditLog, setAuditLog] = useState([]);
    const [auditLogLoading, setAuditLogLoading] = useState(false);
    const [auditLogPage, setAuditLogPage] = useState(1);
    const [auditLogHasMore, setAuditLogHasMore] = useState(false);
    const [appSettings, setAppSettings] = useState({ openRegistration: false });
    const [appSettingsLoading, setAppSettingsLoading] = useState(false);
    const [webhookUrl, setWebhookUrl] = useState('');

    useEffect(() => {
        if (show && auth?.token && auth?.role === 'admin') {
            fetchUsers();
            fetchAppSettings();
        }
    }, [show]);

    useEffect(() => {
        if (!show) return;
        const handleKeyDown = (e) => { if (e.key === 'Escape') onClose(); };
        document.addEventListener('keydown', handleKeyDown);
        return () => document.removeEventListener('keydown', handleKeyDown);
    }, [show, onClose]);

    const fetchUsers = async () => {
        setUserListLoading(true);
        try {
            const res = await fetch('/api/admin/users', { headers: { 'Authorization': `Bearer ${auth.token}` } });
            if (res.ok) {
                const data = await res.json();
                setUserList(data.users || []);
            }
        } catch (err) { console.error('Failed to fetch users:', err); showToast(t('errorOccurred'), 'error'); }
        setUserListLoading(false);
    };

    const fetchAppSettings = async () => {
        try {
            const res = await fetch('/api/admin/app-settings', { headers: { 'Authorization': `Bearer ${auth.token}` } });
            if (res.ok) {
                const data = await res.json();
                const settings = data.settings || { openRegistration: false };
                setAppSettings(settings);
                if (settings.webhookUrl !== undefined) setWebhookUrl(settings.webhookUrl);
            }
        } catch (err) { console.error('Failed to fetch app settings:', err); }
    };

    const handleToggleOpenRegistration = async () => {
        setAppSettingsLoading(true);
        try {
            const res = await fetch('/api/admin/app-settings', {
                method: 'PUT',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' },
                body: JSON.stringify({ openRegistration: !appSettings.openRegistration })
            });
            if (res.ok) {
                const data = await res.json();
                setAppSettings(data.settings);
                showToast(t('settingsSaved'), 'success');
            }
        } catch (err) { console.error('Failed to toggle open registration:', err); showToast(t('errorOccurred'), 'error'); }
        setAppSettingsLoading(false);
    };

    const handleAddUser = async () => {
        if (!newUserName.trim()) return;
        setAddUserLoading(true);
        setAddUserError('');
        setCreatedSetupToken('');
        try {
            const res = await fetch('/api/admin/users', {
                method: 'POST',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' },
                body: JSON.stringify({ username: newUserName.trim(), role: newUserRole })
            });
            const data = await res.json();
            if (res.ok && data.success) {
                showToast(t('userCreated'), 'success');
                setCreatedSetupToken(data.setupToken || '');
                setNewUserName('');
                setNewUserRole('user');
                fetchUsers();
            } else {
                setAddUserError(data.error || t('errorOccurred'));
            }
        } catch (err) {
            setAddUserError(t('errorOccurred'));
        }
        setAddUserLoading(false);
    };

    const handleEditUser = async (uname, resetSetupToken = false) => {
        setEditUserLoading(true);
        setCreatedSetupToken('');
        try {
            const body = { username: uname, role: editUserRole, allowedZones: editAllowedZones };
            if (resetSetupToken) body.resetSetupToken = true;
            const res = await fetch('/api/admin/users', {
                method: 'PUT',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            const data = await res.json();
            if (res.ok) {
                showToast(t('userUpdated'), 'success');
                if (data.setupToken) setCreatedSetupToken(data.setupToken);
                setEditingUser(null);
                fetchUsers();
            }
        } catch (err) { console.error('Failed to edit user:', err); showToast(t('errorOccurred'), 'error'); }
        setEditUserLoading(false);
    };

    const handleDeleteUser = async (uname) => {
        if (!confirm(t('confirmDeleteUser'))) return;
        try {
            const res = await fetch(`/api/admin/users?username=${encodeURIComponent(uname)}`, {
                method: 'DELETE',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' }
            });
            if (res.ok) {
                showToast(t('userDeleted'), 'success');
                fetchUsers();
            }
        } catch (err) { console.error('Failed to delete user:', err); showToast(t('errorOccurred'), 'error'); }
    };

    const fetchAuditLog = async (page = 1) => {
        setAuditLogLoading(true);
        try {
            const res = await fetch(`/api/admin/audit-log?page=${page}&per_page=30`, {
                headers: { 'Authorization': `Bearer ${auth.token}` }
            });
            if (res.ok) {
                const data = await res.json();
                setAuditLog(data.entries || []);
                setAuditLogPage(page);
                setAuditLogHasMore(data.hasMore || false);
            }
        } catch (err) { console.error('Failed to fetch audit log:', err); }
        setAuditLogLoading(false);
    };

    const handleClearAuditLog = async () => {
        if (!confirm(t('auditLogClearConfirm'))) return;
        try {
            const res = await fetch('/api/admin/audit-log', {
                method: 'DELETE',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' }
            });
            if (res.ok) {
                showToast(t('auditLogCleared'), 'success');
                setAuditLog([]);
                setAuditLogPage(1);
                setAuditLogHasMore(false);
            }
        } catch (err) { console.error('Failed to clear audit log:', err); showToast(t('errorOccurred'), 'error'); }
    };

    const handleSaveWebhook = async () => {
        try {
            const res = await fetch('/api/admin/app-settings', {
                method: 'PUT',
                headers: { 'Authorization': `Bearer ${auth.token}`, 'Content-Type': 'application/json' },
                body: JSON.stringify({ webhookUrl })
            });
            if (res.ok) {
                const data = await res.json();
                setAppSettings(data.settings);
                showToast(t('webhookSaved'), 'success');
            }
        } catch (e) { console.error('Failed to save webhook:', e); showToast(t('errorOccurred'), 'error'); }
    };

    if (!show) return null;

    return (
        <div className="modal-overlay" style={{ zIndex: 200 }}
            onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
            <div className="glass-card fade-in modal-content" role="dialog" aria-label={t('usersManagement')} style={{ width: '100%', maxWidth: '580px', padding: '1.5rem', display: 'flex', flexDirection: 'column' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
                    <h3 style={{ fontSize: '1rem', margin: 0 }}>{t('usersManagement')}</h3>
                    <div style={{ display: 'flex', gap: '0.5rem' }}>
                        {tab === 'users' && (
                            <button className="btn btn-primary" style={{ padding: '4px 10px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '4px' }}
                                onClick={() => { setShowAddUser(true); setAddUserError(''); }}>
                                <Plus size={14} /> {t('addUser')}
                            </button>
                        )}
                        <button onClick={onClose} style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '4px', display: 'flex' }} aria-label="Close">
                            <X size={18} color="var(--text-muted)" />
                        </button>
                    </div>
                </div>

                {/* Tab Bar */}
                <div style={{ display: 'flex', gap: '0', marginBottom: '1rem', borderBottom: '2px solid var(--border)' }}>
                    <button
                        onClick={() => setTab('users')}
                        style={{
                            padding: '0.5rem 1rem', fontSize: '0.8rem', fontWeight: 600,
                            border: 'none', background: 'transparent', cursor: 'pointer',
                            color: tab === 'users' ? 'var(--primary)' : 'var(--text-muted)',
                            borderBottom: tab === 'users' ? '2px solid var(--primary)' : '2px solid transparent',
                            marginBottom: '-2px', transition: 'all 0.2s'
                        }}
                    >
                        {t('usersManagement')}
                    </button>
                    <button
                        onClick={() => { setTab('audit'); fetchAuditLog(1); }}
                        style={{
                            padding: '0.5rem 1rem', fontSize: '0.8rem', fontWeight: 600,
                            border: 'none', background: 'transparent', cursor: 'pointer',
                            color: tab === 'audit' ? 'var(--primary)' : 'var(--text-muted)',
                            borderBottom: tab === 'audit' ? '2px solid var(--primary)' : '2px solid transparent',
                            marginBottom: '-2px', transition: 'all 0.2s'
                        }}
                    >
                        {t('auditLog')}
                    </button>
                </div>

                {/* Users Tab */}
                {tab === 'users' && (
                    <>
                        {/* Registration Settings */}
                        <div style={{ padding: '0.75rem 1rem', background: 'var(--hover-bg)', borderRadius: '8px', marginBottom: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
                            <div style={{ flex: 1 }}>
                                <div style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '0.15rem' }}>{t('appSettings')}</div>
                                <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>
                                    {appSettings.openRegistration ? t('openRegistrationDesc') : t('inviteTokenModeDesc')}
                                </div>
                            </div>
                            <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexShrink: 0 }}>
                                <span style={{ fontSize: '0.7rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
                                    {appSettings.openRegistration ? t('openRegistration') : t('inviteTokenMode')}
                                </span>
                                <button
                                    onClick={handleToggleOpenRegistration}
                                    disabled={appSettingsLoading}
                                    style={{
                                        width: '40px', height: '22px', borderRadius: '11px', border: 'none', cursor: 'pointer',
                                        background: appSettings.openRegistration ? 'var(--primary)' : 'var(--toggle-off-bg)',
                                        position: 'relative', transition: 'background 0.2s', flexShrink: 0
                                    }}
                                >
                                    <div style={{
                                        width: '16px', height: '16px', borderRadius: '50%', background: 'var(--toggle-knob)',
                                        position: 'absolute', top: '3px',
                                        left: appSettings.openRegistration ? '21px' : '3px',
                                        transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)'
                                    }} />
                                </button>
                            </div>
                        </div>

                        {/* Webhook URL */}
                        <div style={{ padding: '0.75rem 1rem', background: 'var(--hover-bg)', borderRadius: '8px', marginBottom: '1rem' }}>
                            <div style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '0.25rem' }}>{t('webhookUrl')}</div>
                            <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>{t('webhookDesc')}</div>
                            <div style={{ display: 'flex', gap: '0.5rem' }}>
                                <input type="text" value={webhookUrl}
                                    onChange={e => setWebhookUrl(e.target.value)}
                                    placeholder={t('webhookUrlPlaceholder')}
                                    style={{ flex: 1, fontSize: '0.8rem' }} />
                                <button className="btn btn-primary" style={{ padding: '4px 10px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '4px' }}
                                    onClick={handleSaveWebhook}>
                                    <Save size={12} /> {t('save')}
                                </button>
                            </div>
                        </div>

                        {/* Add User Form */}
                        {showAddUser && (
                            <div style={{ padding: '1rem', background: 'var(--hover-bg)', borderRadius: '8px', marginBottom: '1rem' }}>
                                {createdSetupToken ? (
                                    <div>
                                        <p style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--success)' }}>
                                            <CheckCircle size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
                                            {t('userCreated')}
                                        </p>
                                        <p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
                                            {t('setupTokenLabel')}:
                                        </p>
                                        <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
                                            <input type="text" readOnly value={createdSetupToken}
                                                style={{ flex: 1, fontSize: '0.75rem', fontFamily: 'monospace', background: 'var(--card-bg)', userSelect: 'all' }} />
                                            <button className="btn btn-outline" style={{ padding: '4px 8px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '4px' }}
                                                onClick={() => { navigator.clipboard.writeText(createdSetupToken); showToast(t('setupTokenCopied'), 'success'); }}>
                                                <Copy size={12} /> {t('copied').split(' ')[0]}
                                            </button>
                                        </div>
                                        <p style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
                                            {t('setupAccountDesc')}
                                        </p>
                                        <button className="btn btn-outline" style={{ padding: '4px 10px', fontSize: '0.8rem' }}
                                            onClick={() => { setShowAddUser(false); setCreatedSetupToken(''); }}>
                                            {t('cancel')}
                                        </button>
                                    </div>
                                ) : (
                                    <>
                                    <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
                                        <input type="text" placeholder={t('usernamePlaceholder')} value={newUserName}
                                            onChange={(e) => setNewUserName(e.target.value)}
                                            onKeyDown={(e) => { if (e.key === 'Enter') handleAddUser(); }}
                                            style={{ flex: 1, fontSize: '0.85rem' }} />
                                        <select value={newUserRole} onChange={(e) => setNewUserRole(e.target.value)}
                                            style={{ padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border)', fontSize: '0.85rem' }}>
                                            <option value="user">{t('roleUser')}</option>
                                            <option value="admin">{t('roleAdmin')}</option>
                                        </select>
                                        <button className="btn btn-primary" style={{ padding: '6px 12px', fontSize: '0.8rem' }}
                                            onClick={handleAddUser} disabled={addUserLoading || !newUserName.trim()}>
                                            {addUserLoading ? <RefreshCw className="spin" size={14} /> : t('addUser')}
                                        </button>
                                        <button className="btn btn-outline" style={{ padding: '6px 12px', fontSize: '0.8rem' }}
                                            onClick={() => { setShowAddUser(false); setAddUserError(''); setCreatedSetupToken(''); }}>
                                            {t('cancel')}
                                        </button>
                                    </div>
                                    {addUserError && <p style={{ color: 'var(--error)', fontSize: '0.75rem', margin: 0 }}>{addUserError}</p>}
                                    </>
                                )}
                            </div>
                        )}

                        {/* Setup token display (after reset) */}
                        {!showAddUser && createdSetupToken && (
                            <div style={{ padding: '1rem', background: 'var(--success-bg)', borderRadius: '8px', marginBottom: '1rem', border: '1px solid var(--success-border)' }}>
                                <p style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--success)' }}>
                                    <CheckCircle size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
                                    {t('resetSetupToken')}
                                </p>
                                <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
                                    <input type="text" readOnly value={createdSetupToken}
                                        style={{ flex: 1, fontSize: '0.75rem', fontFamily: 'monospace', background: 'var(--card-bg)', userSelect: 'all' }} />
                                    <button className="btn btn-outline" style={{ padding: '4px 8px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '4px' }}
                                        onClick={() => { navigator.clipboard.writeText(createdSetupToken); showToast(t('setupTokenCopied'), 'success'); }}>
                                        <Copy size={12} /> {t('copied').split(' ')[0]}
                                    </button>
                                </div>
                                <button className="btn btn-outline" style={{ padding: '3px 8px', fontSize: '0.7rem' }}
                                    onClick={() => setCreatedSetupToken('')}>
                                    <X size={12} />
                                </button>
                            </div>
                        )}

                        {/* User List */}
                        <div style={{ overflowY: 'auto', flex: 1 }}>
                            {userListLoading ? (
                                <div style={{ textAlign: 'center', padding: '2rem' }}><RefreshCw className="spin" size={20} color="var(--primary)" /></div>
                            ) : (
                                userList.map(u => (
                                    <div key={u.username} style={{ display: 'flex', alignItems: 'center', padding: '0.6rem 0.75rem', borderBottom: '1px solid var(--border)', gap: '0.5rem' }}>
                                        {editingUser === u.username ? (
                                            <>
                                                <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
                                                    <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                                                        <User size={14} color="var(--text-muted)" />
                                                        <span style={{ fontWeight: 600, fontSize: '0.875rem' }}>{u.username}</span>
                                                    </div>
                                                    <div style={{ display: 'flex', gap: '0.5rem' }}>
                                                        <select value={editUserRole} onChange={(e) => setEditUserRole(e.target.value)}
                                                            style={{ flex: 1, padding: '4px 6px', borderRadius: '6px', border: '1px solid var(--border)', fontSize: '0.8rem' }}>
                                                            <option value="user">{t('roleUser')}</option>
                                                            <option value="admin">{t('roleAdmin')}</option>
                                                        </select>
                                                    </div>
                                                    {/* Zone Permissions */}
                                                    <div style={{ padding: '0.5rem', background: 'var(--hover-bg)', borderRadius: '6px' }}>
                                                        <div style={{ fontSize: '0.7rem', fontWeight: 600, marginBottom: '0.25rem' }}>{t('allowedZones')}</div>
                                                        <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)', marginBottom: '0.35rem' }}>{t('allowedZonesDesc')}</div>
                                                        <div style={{ display: 'flex', gap: '0.35rem', marginBottom: '0.35rem' }}>
                                                            <input
                                                                type="text"
                                                                value={zoneInput}
                                                                onChange={(e) => setZoneInput(e.target.value)}
                                                                onKeyDown={(e) => {
                                                                    if (e.key === 'Enter' || e.key === ',') {
                                                                        e.preventDefault();
                                                                        const val = zoneInput.trim().toLowerCase().replace(/,$/, '');
                                                                        if (val && !editAllowedZones.includes(val)) {
                                                                            setEditAllowedZones([...editAllowedZones, val]);
                                                                        }
                                                                        setZoneInput('');
                                                                    }
                                                                }}
                                                                placeholder={t('allowedZonesPlaceholder')}
                                                                style={{ flex: 1, fontSize: '0.75rem', padding: '3px 6px' }}
                                                            />
                                                            <button
                                                                className="btn btn-outline"
                                                                style={{ padding: '3px 8px', fontSize: '0.65rem' }}
                                                                onClick={() => {
                                                                    const val = zoneInput.trim().toLowerCase().replace(/,$/, '');
                                                                    if (val && !editAllowedZones.includes(val)) {
                                                                        setEditAllowedZones([...editAllowedZones, val]);
                                                                    }
                                                                    setZoneInput('');
                                                                }}
                                                            >
                                                                {t('allowedZonesAdd')}
                                                            </button>
                                                        </div>
                                                        {editAllowedZones.length === 0 ? (
                                                            <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)', fontStyle: 'italic' }}>
                                                                {t('allowedZonesAll')}
                                                            </div>
                                                        ) : (
                                                            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
                                                                {editAllowedZones.map(zone => (
                                                                    <span key={zone} className="badge badge-blue" style={{ fontSize: '0.65rem', padding: '2px 6px', display: 'inline-flex', alignItems: 'center', gap: '3px' }}>
                                                                        {zone}
                                                                        <button
                                                                            onClick={() => setEditAllowedZones(editAllowedZones.filter(z => z !== zone))}
                                                                            style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '0', display: 'flex', lineHeight: 1 }}
                                                                            title={t('allowedZonesRemove')}
                                                                        >
                                                                            <X size={10} />
                                                                        </button>
                                                                    </span>
                                                                ))}
                                                            </div>
                                                        )}
                                                    </div>
                                                    <div style={{ display: 'flex', gap: '0.25rem' }}>
                                                        <button className="btn btn-primary" style={{ padding: '3px 8px', fontSize: '0.7rem' }}
                                                            onClick={() => handleEditUser(u.username)} disabled={editUserLoading}>
                                                            {editUserLoading ? <RefreshCw className="spin" size={12} /> : <Save size={12} />}
                                                        </button>
                                                        <button className="btn btn-outline" style={{ padding: '3px 8px', fontSize: '0.7rem' }}
                                                            onClick={() => handleEditUser(u.username, true)} disabled={editUserLoading}
                                                            title={t('resetSetupToken')}>
                                                            <RefreshCw size={12} />
                                                        </button>
                                                        <button className="btn btn-outline" style={{ padding: '3px 8px', fontSize: '0.7rem' }}
                                                            onClick={() => setEditingUser(null)}>
                                                            <X size={12} />
                                                        </button>
                                                    </div>
                                                </div>
                                            </>
                                        ) : (
                                            <>
                                                <User size={14} color="var(--text-muted)" />
                                                <span style={{ flex: 1, fontWeight: 500, fontSize: '0.875rem' }}>{u.username}</span>
                                                {u.username !== 'admin' && (
                                                    <span className={`badge ${u.status === 'active' ? 'badge-green' : 'badge-orange'}`} style={{ fontSize: '0.6rem', padding: '1px 5px' }}>
                                                        {u.status === 'active' ? t('userStatusActive') : t('userStatusPending')}
                                                    </span>
                                                )}
                                                <span className={`badge ${u.role === 'admin' ? 'badge-orange' : 'badge-green'}`} style={{ fontSize: '0.65rem', padding: '1px 6px' }}>
                                                    {u.role === 'admin' ? t('roleAdmin') : t('roleUser')}
                                                </span>
                                                {u.username !== 'admin' && u.role !== 'admin' && Array.isArray(u.allowedZones) && u.allowedZones.length > 0 && (
                                                    <span className="badge badge-blue" style={{ fontSize: '0.6rem', padding: '1px 5px' }}
                                                        title={u.allowedZones.join(', ')}>
                                                        {u.allowedZones.length} zone{u.allowedZones.length !== 1 ? 's' : ''}
                                                    </span>
                                                )}
                                                {u.username !== 'admin' && (
                                                    <div style={{ display: 'flex', gap: '0.25rem' }}>
                                                        <button style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '4px', display: 'flex', color: 'var(--text-muted)' }}
                                                            onClick={() => { setEditingUser(u.username); setEditUserRole(u.role); setEditAllowedZones(Array.isArray(u.allowedZones) ? [...u.allowedZones] : []); setZoneInput(''); }}
                                                            onMouseEnter={e => e.currentTarget.style.color = 'var(--primary)'}
                                                            onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}
                                                            aria-label={`Edit user ${u.username}`}>
                                                            <Edit2 size={14} />
                                                        </button>
                                                        <button style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '4px', display: 'flex', color: 'var(--text-muted)' }}
                                                            onClick={() => handleDeleteUser(u.username)}
                                                            onMouseEnter={e => e.currentTarget.style.color = 'var(--error)'}
                                                            onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}
                                                            aria-label={`Delete user ${u.username}`}>
                                                            <Trash2 size={14} />
                                                        </button>
                                                    </div>
                                                )}
                                            </>
                                        )}
                                    </div>
                                ))
                            )}
                        </div>
                    </>
                )}

                {/* Audit Log Tab */}
                {tab === 'audit' && (
                    <div style={{ overflowY: 'auto', flex: 1 }}>
                        <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '0.75rem' }}>
                            <button
                                className="btn btn-outline"
                                onClick={handleClearAuditLog}
                                style={{ padding: '4px 10px', fontSize: '0.7rem', display: 'flex', alignItems: 'center', gap: '4px', color: 'var(--error)', borderColor: 'var(--error)' }}
                            >
                                <Trash2 size={12} /> {t('auditLogClear')}
                            </button>
                        </div>

                        {auditLogLoading ? (
                            <div style={{ textAlign: 'center', padding: '2rem' }}>
                                <RefreshCw className="spin" size={20} color="var(--primary)" />
                            </div>
                        ) : auditLog.length === 0 ? (
                            <p style={{ textAlign: 'center', padding: '2rem 0', fontSize: '0.85rem', color: 'var(--text-muted)' }}>
                                {t('auditLogEmpty')}
                            </p>
                        ) : (
                            <>
                            <div style={{ overflowX: 'auto' }}>
                                <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
                                    <thead>
                                        <tr style={{ borderBottom: '2px solid var(--border)' }}>
                                            <th style={{ padding: '0.5rem 0.6rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600, whiteSpace: 'nowrap' }}>{t('auditTime')}</th>
                                            <th style={{ padding: '0.5rem 0.6rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600, whiteSpace: 'nowrap' }}>{t('auditUser')}</th>
                                            <th style={{ padding: '0.5rem 0.6rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600, whiteSpace: 'nowrap' }}>{t('auditAction')}</th>
                                            <th style={{ padding: '0.5rem 0.6rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600 }}>{t('auditDetail')}</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        {auditLog.map((entry, idx) => (
                                            <tr key={idx} style={{ borderBottom: '1px solid var(--border)' }}>
                                                <td style={{ padding: '0.5rem 0.6rem', whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>
                                                    {new Date(entry.timestamp).toLocaleString()}
                                                </td>
                                                <td style={{ padding: '0.5rem 0.6rem', fontWeight: 500 }}>
                                                    {entry.username}
                                                </td>
                                                <td style={{ padding: '0.5rem 0.6rem' }}>
                                                    <span className="badge badge-blue" style={{ fontSize: '0.6rem', padding: '1px 6px' }}>
                                                        {entry.action}
                                                    </span>
                                                </td>
                                                <td style={{ padding: '0.5rem 0.6rem', color: 'var(--text-muted)', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                                    {entry.detail}
                                                </td>
                                            </tr>
                                        ))}
                                    </tbody>
                                </table>
                            </div>

                            {/* Pagination */}
                            <div style={{ display: 'flex', justifyContent: 'center', gap: '0.5rem', marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}>
                                <button className="btn btn-outline" onClick={() => fetchAuditLog(auditLogPage - 1)}
                                    disabled={auditLogPage <= 1 || auditLogLoading}
                                    style={{ padding: '4px 12px', fontSize: '0.75rem' }}>
                                    {t('auditPrev')}
                                </button>
                                <span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', padding: '0 0.5rem' }}>
                                    {auditLogPage}
                                </span>
                                <button className="btn btn-outline" onClick={() => fetchAuditLog(auditLogPage + 1)}
                                    disabled={!auditLogHasMore || auditLogLoading}
                                    style={{ padding: '4px 12px', fontSize: '0.75rem' }}>
                                    {t('auditNext')}
                                </button>
                            </div>
                            </>
                        )}
                    </div>
                )}
            </div>
        </div>
    );
};

export default UserManagement;