Surface actual error message when user creation fails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,10 @@ export default function Companies() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showNew, setShowNew] = useState(false);
|
const [showNew, setShowNew] = useState(false);
|
||||||
const [newForm, setNewForm] = useState({ name: '', phone: '', address: '' });
|
const [newForm, setNewForm] = useState({ name: '', phone: '', address: '' });
|
||||||
|
const [showNewUser, setShowNewUser] = useState(false);
|
||||||
|
const [userForm, setUserForm] = useState({ name: '', email: '', password: '', company_id: '' });
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [userError, setUserError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
@@ -49,6 +52,29 @@ export default function Companies() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setUserError('');
|
||||||
|
setSaving(true);
|
||||||
|
const { data, error } = await supabase.functions.invoke('create-user', {
|
||||||
|
body: {
|
||||||
|
name: userForm.name.trim(),
|
||||||
|
email: userForm.email.trim(),
|
||||||
|
password: userForm.password,
|
||||||
|
company_id: userForm.company_id || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSaving(false);
|
||||||
|
const errMsg = data?.error || error?.message || (error ? JSON.stringify(error) : null);
|
||||||
|
if (errMsg) {
|
||||||
|
setUserError(errMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowNewUser(false);
|
||||||
|
setUserForm({ name: '', email: '', password: '', company_id: '' });
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
||||||
|
|
||||||
const unassigned = profiles.filter(p => !p.company_id);
|
const unassigned = profiles.filter(p => !p.company_id);
|
||||||
@@ -59,7 +85,7 @@ export default function Companies() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="page-title">Companies</div>
|
<div className="page-title">Companies</div>
|
||||||
<div className="page-subtitle">
|
<div className="page-subtitle">
|
||||||
{companies.length} company{companies.length !== 1 ? 'ies' : ''}
|
{companies.length} {companies.length !== 1 ? 'companies' : 'company'}
|
||||||
{unassigned.length > 0 && (
|
{unassigned.length > 0 && (
|
||||||
<span style={{ marginLeft: 10, color: 'var(--danger)', fontWeight: 600 }}>
|
<span style={{ marginLeft: 10, color: 'var(--danger)', fontWeight: 600 }}>
|
||||||
· {unassigned.length} unassigned user{unassigned.length !== 1 ? 's' : ''}
|
· {unassigned.length} unassigned user{unassigned.length !== 1 ? 's' : ''}
|
||||||
@@ -67,10 +93,56 @@ export default function Companies() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary" onClick={() => setShowNew(s => !s)}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button className="btn btn-outline" onClick={() => { setShowNewUser(s => !s); setShowNew(false); setUserError(''); }}>
|
||||||
|
{showNewUser ? 'Cancel' : '+ New User'}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={() => { setShowNew(s => !s); setShowNewUser(false); }}>
|
||||||
{showNew ? 'Cancel' : '+ New Company'}
|
{showNew ? 'Cancel' : '+ New Company'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showNewUser && (
|
||||||
|
<div className="card" style={{ marginBottom: 24, maxWidth: 480 }}>
|
||||||
|
<div className="card-title">New User</div>
|
||||||
|
<form onSubmit={handleCreateUser}>
|
||||||
|
<div className="grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Full Name *</label>
|
||||||
|
<input type="text" placeholder="Jane Smith" value={userForm.name}
|
||||||
|
onChange={e => setUserForm(f => ({ ...f, name: e.target.value }))} required autoFocus />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Email *</label>
|
||||||
|
<input type="email" placeholder="jane@acme.com" value={userForm.email}
|
||||||
|
onChange={e => setUserForm(f => ({ ...f, email: e.target.value }))} required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-2">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Password *</label>
|
||||||
|
<input type="password" placeholder="Temporary password" value={userForm.password}
|
||||||
|
onChange={e => setUserForm(f => ({ ...f, password: e.target.value }))} required minLength={6} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Assign to Company</label>
|
||||||
|
<select value={userForm.company_id} onChange={e => setUserForm(f => ({ ...f, company_id: e.target.value }))}>
|
||||||
|
<option value="">No company yet</option>
|
||||||
|
{companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{userError && <p style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 12 }}>{userError}</p>}
|
||||||
|
<div className="action-buttons">
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||||
|
{saving ? 'Creating...' : 'Create User'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-outline" onClick={() => setShowNewUser(false)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showNew && (
|
{showNew && (
|
||||||
<div className="card" style={{ marginBottom: 24, maxWidth: 480 }}>
|
<div className="card" style={{ marginBottom: 24, maxWidth: 480 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user