Compare commits
3 Commits
2c0152f176
...
6cdec2b4b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cdec2b4b5 | ||
|
|
4d91e8e8a8 | ||
|
|
b1f7d574ed |
@@ -200,17 +200,6 @@ function AppContent() {
|
|||||||
placeholder={t('team.fullName')}
|
placeholder={t('team.fullName')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.teamRole')}</label>
|
|
||||||
<select
|
|
||||||
value={profileForm.team_role}
|
|
||||||
onChange={e => setProfileForm(f => ({ ...f, team_role: e.target.value }))}
|
|
||||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
|
||||||
>
|
|
||||||
<option value="">—</option>
|
|
||||||
{TEAM_ROLES.map(r => <option key={r.value} value={r.value}>{r.label}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.phone')} {t('team.optional')}</label>
|
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.phone')} {t('team.optional')}</label>
|
||||||
<input
|
<input
|
||||||
@@ -220,16 +209,6 @@ function AppContent() {
|
|||||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.brands')}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={profileForm.brands}
|
|
||||||
onChange={e => setProfileForm(f => ({ ...f, brands: e.target.value }))}
|
|
||||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
|
||||||
placeholder={t('team.brandsHelp')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
|
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowProfileModal(false)}
|
onClick={() => setShowProfileModal(false)}
|
||||||
@@ -241,15 +220,9 @@ function AppContent() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setProfileSaving(true)
|
setProfileSaving(true)
|
||||||
try {
|
try {
|
||||||
const brandsArr = profileForm.brands
|
|
||||||
.split(',')
|
|
||||||
.map(b => b.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
await api.patch('/users/me/profile', {
|
await api.patch('/users/me/profile', {
|
||||||
name: profileForm.name,
|
name: profileForm.name,
|
||||||
team_role: profileForm.team_role,
|
|
||||||
phone: profileForm.phone || null,
|
phone: profileForm.phone || null,
|
||||||
brands: brandsArr,
|
|
||||||
})
|
})
|
||||||
await checkAuth()
|
await checkAuth()
|
||||||
setShowProfileModal(false)
|
setShowProfileModal(false)
|
||||||
@@ -260,7 +233,7 @@ function AppContent() {
|
|||||||
setProfileSaving(false)
|
setProfileSaving(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!profileForm.name || !profileForm.team_role || profileSaving}
|
disabled={!profileForm.name || profileSaving}
|
||||||
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||||
>
|
>
|
||||||
{profileSaving ? t('common.loading') : t('team.saveProfile')}
|
{profileSaving ? t('common.loading') : t('team.saveProfile')}
|
||||||
|
|||||||
@@ -235,7 +235,14 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.teamRole')}</label>
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.teamRole')}</label>
|
||||||
{userRole === 'manager' && isCreateMode && !isEditingSelf ? (
|
{isEditingSelf ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={ROLES.find(r => r.value === form.role)?.label || form.role || '—'}
|
||||||
|
disabled
|
||||||
|
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface-tertiary text-text-tertiary cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
) : userRole === 'manager' && isCreateMode ? (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -269,6 +276,11 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
|
|||||||
|
|
||||||
<div ref={brandsDropdownRef} className="relative">
|
<div ref={brandsDropdownRef} className="relative">
|
||||||
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.brands')}</label>
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.brands')}</label>
|
||||||
|
{isEditingSelf ? (
|
||||||
|
<div className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface-tertiary text-text-tertiary cursor-not-allowed">
|
||||||
|
{(form.brands || []).length === 0 ? '—' : (form.brands || []).join(', ')}
|
||||||
|
</div>
|
||||||
|
) : <>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowBrandsDropdown(prev => !prev)}
|
onClick={() => setShowBrandsDropdown(prev => !prev)}
|
||||||
@@ -328,6 +340,7 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modules toggle */}
|
{/* Modules toggle */}
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export default function Team() {
|
|||||||
if (isEditingSelf) {
|
if (isEditingSelf) {
|
||||||
await api.patch('/users/me/profile', {
|
await api.patch('/users/me/profile', {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
team_role: data.role,
|
|
||||||
brands: data.brands,
|
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -83,8 +81,8 @@ export default function Team() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTeam()
|
await loadTeam()
|
||||||
loadTeams()
|
await loadTeams()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Save failed:', err)
|
console.error('Save failed:', err)
|
||||||
alert(err.message || 'Failed to save')
|
alert(err.message || 'Failed to save')
|
||||||
@@ -98,8 +96,8 @@ export default function Team() {
|
|||||||
} else {
|
} else {
|
||||||
await api.post('/teams', data)
|
await api.post('/teams', data)
|
||||||
}
|
}
|
||||||
loadTeams()
|
await loadTeams()
|
||||||
loadTeam()
|
await loadTeam()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Team save failed:', err)
|
console.error('Team save failed:', err)
|
||||||
alert(err.message || 'Failed to save team')
|
alert(err.message || 'Failed to save team')
|
||||||
@@ -111,8 +109,8 @@ export default function Team() {
|
|||||||
await api.delete(`/teams/${teamId}`)
|
await api.delete(`/teams/${teamId}`)
|
||||||
setPanelTeam(null)
|
setPanelTeam(null)
|
||||||
if (teamFilter === teamId) setTeamFilter(null)
|
if (teamFilter === teamId) setTeamFilter(null)
|
||||||
loadTeams()
|
await loadTeams()
|
||||||
loadTeam()
|
await loadTeam()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Team delete failed:', err)
|
console.error('Team delete failed:', err)
|
||||||
}
|
}
|
||||||
@@ -124,7 +122,7 @@ export default function Team() {
|
|||||||
setSelectedMember(null)
|
setSelectedMember(null)
|
||||||
}
|
}
|
||||||
setPanelMember(null)
|
setPanelMember(null)
|
||||||
loadTeam()
|
await loadTeam()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openMemberDetail = async (member) => {
|
const openMemberDetail = async (member) => {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export default function Users() {
|
|||||||
const [form, setForm] = useState(EMPTY_FORM)
|
const [form, setForm] = useState(EMPTY_FORM)
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
const [userToDelete, setUserToDelete] = useState(null)
|
const [userToDelete, setUserToDelete] = useState(null)
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [passwordError, setPasswordError] = useState('')
|
||||||
|
|
||||||
useEffect(() => { loadUsers() }, [])
|
useEffect(() => { loadUsers() }, [])
|
||||||
|
|
||||||
@@ -49,6 +51,11 @@ export default function Users() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
setPasswordError('')
|
||||||
|
if (form.password && form.password !== confirmPassword) {
|
||||||
|
setPasswordError('Passwords do not match')
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
@@ -87,12 +94,16 @@ export default function Users() {
|
|||||||
role: user.role || 'contributor',
|
role: user.role || 'contributor',
|
||||||
avatar: user.avatar || '',
|
avatar: user.avatar || '',
|
||||||
})
|
})
|
||||||
|
setConfirmPassword('')
|
||||||
|
setPasswordError('')
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openNew = () => {
|
const openNew = () => {
|
||||||
setEditingUser(null)
|
setEditingUser(null)
|
||||||
setForm(EMPTY_FORM)
|
setForm(EMPTY_FORM)
|
||||||
|
setConfirmPassword('')
|
||||||
|
setPasswordError('')
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,13 +264,29 @@ export default function Users() {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
|
onChange={e => { setForm(f => ({ ...f, password: e.target.value })); setPasswordError('') }}
|
||||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
required={!editingUser}
|
required={!editingUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{form.password && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-text-primary mb-1">Confirm Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={e => { setConfirmPassword(e.target.value); setPasswordError('') }}
|
||||||
|
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
{passwordError && (
|
||||||
|
<p className="text-xs text-red-500 mt-1">{passwordError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-text-primary mb-1">Role *</label>
|
<label className="block text-sm font-medium text-text-primary mb-1">Role *</label>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
team_role: user.team_role,
|
team_role: user.team_role,
|
||||||
tutorial_completed: user.tutorial_completed,
|
tutorial_completed: user.tutorial_completed,
|
||||||
profileComplete: !!user.team_role,
|
profileComplete: !!user.name,
|
||||||
modules,
|
modules,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -739,9 +739,7 @@ app.get('/api/users/me/profile', requireAuth, async (req, res) => {
|
|||||||
app.patch('/api/users/me/profile', requireAuth, async (req, res) => {
|
app.patch('/api/users/me/profile', requireAuth, async (req, res) => {
|
||||||
const data = {};
|
const data = {};
|
||||||
if (req.body.name !== undefined) data.name = req.body.name;
|
if (req.body.name !== undefined) data.name = req.body.name;
|
||||||
if (req.body.team_role !== undefined) data.team_role = req.body.team_role;
|
|
||||||
if (req.body.phone !== undefined) data.phone = req.body.phone;
|
if (req.body.phone !== undefined) data.phone = req.body.phone;
|
||||||
if (req.body.brands !== undefined) data.brands = JSON.stringify(req.body.brands);
|
|
||||||
|
|
||||||
if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });
|
if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user