readysite / website / frontend / pages / Users.jsx
13.2 KB
Users.jsx
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { apiGet, apiPost, apiPatch, apiDelete } from '../hooks/useAPI.js';
import { useChat } from '../layouts/AdminLayout.jsx';

export function Users() {
  const { toggleChat } = useChat() || {};
  const [users, setUsers] = useState(null);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(1);
  const [showModal, setShowModal] = useState(false);
  const [editUser, setEditUser] = useState(null);
  const [saving, setSaving] = useState(false);
  const [signupEnabled, setSignupEnabled] = useState(false);

  const fetchUsers = useCallback(() => {
    setLoading(true);
    Promise.all([
      apiGet(`/api/users?page=${page}`),
      apiGet('/api/admin/settings'),
    ]).then(([usersData, settings]) => {
      setUsers(usersData);
      setSignupEnabled(settings?.signupEnabled || false);
    }).finally(() => setLoading(false));
  }, [page]);

  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  const toggleSignup = async () => {
    const newValue = !signupEnabled;
    setSignupEnabled(newValue);
    try {
      await apiPatch('/api/admin/settings', { signupEnabled: newValue });
    } catch {
      setSignupEnabled(!newValue); // Revert on error
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const form = e.target;
    const formData = new FormData(form);

    setSaving(true);
    try {
      if (editUser) {
        await apiPost(`/api/users/${editUser.id}`, {
          name: formData.get('name'),
          email: formData.get('email'),
          role: formData.get('role'),
        });
      } else {
        await apiPost('/api/users', {
          name: formData.get('name'),
          email: formData.get('email'),
          password: formData.get('password'),
          role: formData.get('role'),
        });
      }
      setShowModal(false);
      setEditUser(null);
      form.reset();
      fetchUsers();
    } finally {
      setSaving(false);
    }
  };

  const openEdit = (user) => {
    setEditUser(user);
    setShowModal(true);
  };

  const openNew = () => {
    setEditUser(null);
    setShowModal(true);
  };

  const pagination = users?.pagination || { totalItems: 0, totalPages: 1, page: 1 };

  return (
    <div className="flex flex-col h-full">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3 border-b border-base-300 bg-base-200/50">
        <div>
          <h2 className="font-semibold">Users</h2>
          <p className="text-xs text-base-content/50">User accounts</p>
        </div>
        <div className="flex items-center gap-3">
          {/* Signup Toggle */}
          <label className="flex items-center gap-2 cursor-pointer">
            <span className="text-sm text-base-content/70">Public signup</span>
            <input
              type="checkbox"
              className="toggle toggle-sm toggle-primary"
              checked={signupEnabled}
              onChange={toggleSignup}
            />
          </label>

          <div className="w-px h-6 bg-base-300" />

          <button onClick={openNew} className="btn btn-ghost btn-sm gap-1">
            <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
            </svg>
            User
          </button>

          {/* AI Assistant */}
          {toggleChat && (
            <button onClick={toggleChat} className="btn btn-soft btn-primary btn-sm gap-2">
              <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
              </svg>
              AI Assistant
            </button>
          )}
        </div>
      </div>

      {/* Content */}
      <div className="flex-1 overflow-auto p-4">
        {loading ? (
          <div className="flex justify-center py-12">
            <span className="loading loading-spinner loading-lg" />
          </div>
        ) : users?.items?.length ? (
          <div className="max-w-4xl mx-auto">
            <div className="overflow-x-auto">
              <table className="table table-zebra w-full">
                <thead>
                  <tr>
                    <th>User</th>
                    <th>Role</th>
                    <th>Status</th>
                    <th>Created</th>
                    <th className="w-24"></th>
                  </tr>
                </thead>
                <tbody>
                  {users.items.map((user) => (
                    <tr key={user.id} className="hover">
                      <td>
                        <div className="flex items-center gap-3">
                          <div className={user.avatarUrl ? "avatar" : "avatar avatar-placeholder"}>
                            <div className="w-8 rounded-full bg-neutral text-neutral-content">
                              {user.avatarUrl ? (
                                <img src={user.avatarUrl} alt={user.name} />
                              ) : (
                                <span className="text-xs">{(user.email || '?')[0].toUpperCase()}</span>
                              )}
                            </div>
                          </div>
                          <div>
                            <div className="font-medium">{user.name || user.email}</div>
                            {user.name && <div className="text-xs text-base-content/50">{user.email}</div>}
                          </div>
                        </div>
                      </td>
                      <td>
                        {user.role === 'admin' ? (
                          <span className="badge badge-primary badge-sm">Admin</span>
                        ) : user.role === 'user' ? (
                          <span className="badge badge-ghost badge-sm">User</span>
                        ) : (
                          <span className="badge badge-ghost badge-sm">{user.role}</span>
                        )}
                      </td>
                      <td>
                        {user.verified ? (
                          <span className="badge badge-success badge-sm gap-1">
                            <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
                            </svg>
                            Verified
                          </span>
                        ) : (
                          <span className="badge badge-warning badge-sm">Pending</span>
                        )}
                      </td>
                      <td className="text-sm text-base-content/50">
                        {new Date(user.created).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
                      </td>
                      <td>
                        <div className="flex gap-1 justify-end">
                          <button onClick={() => openEdit(user)} className="btn btn-ghost btn-xs">
                            Edit
                          </button>
                          <button
                            onClick={async () => {
                              if (!confirm('Delete this user?')) return;
                              await apiDelete(`/api/users/${user.id}`);
                              fetchUsers();
                            }}
                            className="btn btn-ghost btn-xs text-error"
                          >
                            <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                            </svg>
                          </button>
                        </div>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>

            {/* Pagination */}
            {pagination.totalPages > 1 && (
              <div className="flex justify-center mt-6">
                <div className="join">
                  {page > 1 ? (
                    <button onClick={() => setPage(page - 1)} className="join-item btn btn-sm">Previous</button>
                  ) : (
                    <button className="join-item btn btn-sm btn-disabled">Previous</button>
                  )}

                  {Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((pageNum) => (
                    <button
                      key={pageNum}
                      onClick={() => setPage(pageNum)}
                      className={`join-item btn btn-sm ${pageNum === page ? 'btn-active' : ''}`}
                    >
                      {pageNum}
                    </button>
                  ))}

                  {page < pagination.totalPages ? (
                    <button onClick={() => setPage(page + 1)} className="join-item btn btn-sm">Next</button>
                  ) : (
                    <button className="join-item btn btn-sm btn-disabled">Next</button>
                  )}
                </div>
              </div>
            )}
          </div>
        ) : (
          <div className="flex flex-col items-center justify-center h-full text-center px-6">
            <div className="w-16 h-16 rounded-2xl bg-base-200 flex items-center justify-center mb-4">
              <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
              </svg>
            </div>
            <h3 className="text-lg font-semibold mb-1">No users yet</h3>
            <p className="text-base-content/50 text-sm mb-4">Create the first user to get started.</p>
            <button onClick={openNew} className="btn btn-primary btn-sm">Create First User</button>
          </div>
        )}
      </div>

      {/* User Modal */}
      {showModal && (
        <dialog className="modal modal-open">
          <div className="modal-box w-11/12 max-w-lg">
            <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onClick={() => { setShowModal(false); setEditUser(null); }}>✕</button>
            <h3 className="font-bold text-lg mb-4">{editUser ? 'Edit User' : 'New User'}</h3>

            <form onSubmit={handleSubmit} className="space-y-4">
              <div className="form-control">
                <label className="floating-label">
                  <span>Name</span>
                  <input type="text" name="name" placeholder="Full Name" className="input input-bordered w-full" defaultValue={editUser?.name || ''} />
                </label>
              </div>

              <div className="form-control">
                <label className="floating-label">
                  <span>Email</span>
                  <input type="email" name="email" placeholder="email@example.com" className="input input-bordered w-full" required defaultValue={editUser?.email || ''} />
                </label>
              </div>

              {!editUser && (
                <div className="form-control">
                  <label className="floating-label">
                    <span>Password</span>
                    <input type="password" name="password" placeholder="Password" className="input input-bordered w-full" required />
                  </label>
                </div>
              )}

              <div className="form-control">
                <label className="label">
                  <span className="label-text text-xs">Role</span>
                </label>
                <select name="role" className="select select-bordered select-sm w-full" defaultValue={editUser?.role || 'user'}>
                  <option value="user">User</option>
                  <option value="admin">Admin</option>
                </select>
              </div>

              <div className="flex justify-end gap-2 pt-4">
                <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setShowModal(false); setEditUser(null); }}>Cancel</button>
                <button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
                  {saving ? <span className="loading loading-spinner loading-sm" /> : (editUser ? 'Save Changes' : 'Create User')}
                </button>
              </div>
            </form>
          </div>
          <div className="modal-backdrop" onClick={() => { setShowModal(false); setEditUser(null); }} />
        </dialog>
      )}
    </div>
  );
}
← Back