feat: slide panels, task calendar, team management, project editing, collapsible sections
- Add SlidePanel, TaskDetailPanel, PostDetailPanel, TeamPanel, TeamMemberPanel - Add ProjectEditPanel, CollapsibleSection, DatePresetPicker, TaskCalendarView - Update App, AuthContext, i18n (ar/en), PostProduction, ProjectDetail, Projects - Update Settings, Tasks, Team pages - Update InteractiveTimeline, MemberCard, ProjectCard, TaskCard components - Update server API utilities - Remove tracked server/node_modules (now properly gitignored)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { Plus, Users, ArrowLeft, User as UserIcon } from 'lucide-react'
|
||||
import { Plus, Users, ArrowLeft, User as UserIcon, Edit2, LayoutGrid, Network } from 'lucide-react'
|
||||
import { getInitials } from '../utils/api'
|
||||
import { AppContext } from '../App'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
@@ -7,110 +8,125 @@ import { api } from '../utils/api'
|
||||
import MemberCard from '../components/MemberCard'
|
||||
import StatusBadge from '../components/StatusBadge'
|
||||
import BrandBadge from '../components/BrandBadge'
|
||||
import Modal from '../components/Modal'
|
||||
|
||||
const EMPTY_MEMBER = {
|
||||
name: '', email: '', password: '', role: 'content_writer', brands: '', phone: '',
|
||||
}
|
||||
|
||||
const ROLES = [
|
||||
{ value: 'manager', label: 'Manager' },
|
||||
{ value: 'approver', label: 'Approver' },
|
||||
{ value: 'publisher', label: 'Publisher' },
|
||||
{ value: 'content_creator', label: 'Content Creator' },
|
||||
{ value: 'producer', label: 'Producer' },
|
||||
{ value: 'designer', label: 'Designer' },
|
||||
{ value: 'content_writer', label: 'Content Writer' },
|
||||
{ value: 'social_media_manager', label: 'Social Media Manager' },
|
||||
{ value: 'photographer', label: 'Photographer' },
|
||||
{ value: 'videographer', label: 'Videographer' },
|
||||
{ value: 'strategist', label: 'Strategist' },
|
||||
]
|
||||
import TeamMemberPanel from '../components/TeamMemberPanel'
|
||||
import TeamPanel from '../components/TeamPanel'
|
||||
|
||||
export default function Team() {
|
||||
const { t } = useLanguage()
|
||||
const { teamMembers, loadTeam, currentUser } = useContext(AppContext)
|
||||
const { teamMembers, loadTeam, currentUser, teams, loadTeams, brands } = useContext(AppContext)
|
||||
const { user } = useAuth()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [editingMember, setEditingMember] = useState(null)
|
||||
const [isEditingSelf, setIsEditingSelf] = useState(false)
|
||||
const [formData, setFormData] = useState(EMPTY_MEMBER)
|
||||
const [panelMember, setPanelMember] = useState(null)
|
||||
const [panelIsEditingSelf, setPanelIsEditingSelf] = useState(false)
|
||||
const [selectedMember, setSelectedMember] = useState(null)
|
||||
const [memberTasks, setMemberTasks] = useState([])
|
||||
const [memberPosts, setMemberPosts] = useState([])
|
||||
const [loadingDetail, setLoadingDetail] = useState(false)
|
||||
const [panelTeam, setPanelTeam] = useState(null)
|
||||
const [teamFilter, setTeamFilter] = useState(null)
|
||||
const [viewMode, setViewMode] = useState('grid') // 'grid' | 'teams'
|
||||
|
||||
const canManageTeam = user?.role === 'superadmin' || user?.role === 'manager'
|
||||
|
||||
const openNew = () => {
|
||||
setEditingMember(null)
|
||||
setIsEditingSelf(false)
|
||||
setFormData(EMPTY_MEMBER)
|
||||
setShowModal(true)
|
||||
setPanelMember({ role: 'content_writer' })
|
||||
setPanelIsEditingSelf(false)
|
||||
}
|
||||
|
||||
const openEdit = (member) => {
|
||||
const isSelf = member._id === user?.id || member.id === user?.id
|
||||
setEditingMember(member)
|
||||
setIsEditingSelf(isSelf)
|
||||
setFormData({
|
||||
name: member.name || '',
|
||||
email: member.email || '',
|
||||
password: '',
|
||||
role: member.team_role || member.role || 'content_writer',
|
||||
brands: Array.isArray(member.brands) ? member.brands.join(', ') : (member.brands || ''),
|
||||
phone: member.phone || '',
|
||||
})
|
||||
setShowModal(true)
|
||||
setPanelMember(member)
|
||||
setPanelIsEditingSelf(isSelf)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const handlePanelSave = async (memberId, data, isEditingSelf) => {
|
||||
try {
|
||||
const brands = typeof formData.brands === 'string'
|
||||
? formData.brands.split(',').map(b => b.trim()).filter(Boolean)
|
||||
: formData.brands
|
||||
|
||||
// If editing self, use self-service endpoint
|
||||
if (isEditingSelf) {
|
||||
const data = {
|
||||
name: formData.name,
|
||||
team_role: formData.role,
|
||||
brands,
|
||||
phone: formData.phone,
|
||||
}
|
||||
await api.patch('/users/me/profile', data)
|
||||
await api.patch('/users/me/profile', {
|
||||
name: data.name,
|
||||
team_role: data.role,
|
||||
brands: data.brands,
|
||||
phone: data.phone,
|
||||
})
|
||||
} else {
|
||||
// Manager/superadmin creating or editing other users
|
||||
const data = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
team_role: formData.role,
|
||||
brands,
|
||||
phone: formData.phone,
|
||||
const payload = {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
team_role: data.role,
|
||||
brands: data.brands,
|
||||
phone: data.phone,
|
||||
modules: data.modules,
|
||||
}
|
||||
if (formData.password) {
|
||||
data.password = formData.password
|
||||
}
|
||||
|
||||
if (editingMember) {
|
||||
await api.patch(`/users/team/${editingMember._id}`, data)
|
||||
if (data.password) payload.password = data.password
|
||||
|
||||
if (memberId) {
|
||||
await api.patch(`/users/team/${memberId}`, payload)
|
||||
} else {
|
||||
await api.post('/users/team', data)
|
||||
const created = await api.post('/users/team', payload)
|
||||
memberId = created?.id || created?.Id
|
||||
}
|
||||
}
|
||||
|
||||
setShowModal(false)
|
||||
setEditingMember(null)
|
||||
setIsEditingSelf(false)
|
||||
setFormData(EMPTY_MEMBER)
|
||||
|
||||
// Sync team memberships if team_ids provided
|
||||
if (data.team_ids !== undefined && memberId && !isEditingSelf) {
|
||||
const member = teamMembers.find(m => (m.id || m._id) === memberId)
|
||||
const currentTeamIds = member?.teams ? member.teams.map(t => t.id) : []
|
||||
const targetTeamIds = data.team_ids || []
|
||||
|
||||
const toAdd = targetTeamIds.filter(id => !currentTeamIds.includes(id))
|
||||
const toRemove = currentTeamIds.filter(id => !targetTeamIds.includes(id))
|
||||
|
||||
for (const teamId of toAdd) {
|
||||
await api.post(`/teams/${teamId}/members`, { user_id: memberId })
|
||||
}
|
||||
for (const teamId of toRemove) {
|
||||
await api.delete(`/teams/${teamId}/members/${memberId}`)
|
||||
}
|
||||
}
|
||||
|
||||
loadTeam()
|
||||
loadTeams()
|
||||
} catch (err) {
|
||||
console.error('Save failed:', err)
|
||||
alert(err.message || 'Failed to save')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTeamSave = async (teamId, data) => {
|
||||
try {
|
||||
if (teamId) {
|
||||
await api.patch(`/teams/${teamId}`, data)
|
||||
} else {
|
||||
await api.post('/teams', data)
|
||||
}
|
||||
loadTeams()
|
||||
loadTeam()
|
||||
} catch (err) {
|
||||
console.error('Team save failed:', err)
|
||||
alert(err.message || 'Failed to save team')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTeamDelete = async (teamId) => {
|
||||
try {
|
||||
await api.delete(`/teams/${teamId}`)
|
||||
setPanelTeam(null)
|
||||
if (teamFilter === teamId) setTeamFilter(null)
|
||||
loadTeams()
|
||||
loadTeam()
|
||||
} catch (err) {
|
||||
console.error('Team delete failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePanelDelete = async (memberId) => {
|
||||
await api.delete(`/users/team/${memberId}`)
|
||||
if (selectedMember?._id === memberId) {
|
||||
setSelectedMember(null)
|
||||
}
|
||||
setPanelMember(null)
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
const openMemberDetail = async (member) => {
|
||||
setSelectedMember(member)
|
||||
setLoadingDetail(true)
|
||||
@@ -243,18 +259,67 @@ export default function Team() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Member Panel */}
|
||||
{panelMember && (
|
||||
<TeamMemberPanel
|
||||
member={panelMember}
|
||||
isEditingSelf={panelIsEditingSelf}
|
||||
onClose={() => setPanelMember(null)}
|
||||
onSave={handlePanelSave}
|
||||
onDelete={canManageTeam ? handlePanelDelete : null}
|
||||
canManageTeam={canManageTeam}
|
||||
userRole={user?.role}
|
||||
teams={teams}
|
||||
brands={brands}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const displayedMembers = teamFilter
|
||||
? teamMembers.filter(m => m.teams?.some(t => t.id === teamFilter))
|
||||
: teamMembers
|
||||
|
||||
// Members not in any team
|
||||
const unassignedMembers = teamMembers.filter(m => !m.teams || m.teams.length === 0)
|
||||
|
||||
const avatarColors = [
|
||||
'from-indigo-400 to-purple-500',
|
||||
'from-pink-400 to-rose-500',
|
||||
'from-emerald-400 to-teal-500',
|
||||
'from-amber-400 to-orange-500',
|
||||
'from-cyan-400 to-blue-500',
|
||||
]
|
||||
|
||||
// Team grid
|
||||
return (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{teamMembers.length} {teamMembers.length !== 1 ? t('team.membersPlural') : t('team.member')}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{displayedMembers.length} {displayedMembers.length !== 1 ? t('team.membersPlural') : t('team.member')}
|
||||
</p>
|
||||
{/* View toggle */}
|
||||
<div className="flex items-center bg-white border border-border rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 transition-colors ${viewMode === 'grid' ? 'bg-brand-primary text-white' : 'text-text-tertiary hover:text-text-primary'}`}
|
||||
title={t('team.gridView')}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('teams')}
|
||||
className={`p-2 transition-colors ${viewMode === 'teams' ? 'bg-brand-primary text-white' : 'text-text-tertiary hover:text-text-primary'}`}
|
||||
title={t('team.teamsView')}
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{/* Edit own profile button */}
|
||||
<button
|
||||
@@ -267,7 +332,18 @@ export default function Team() {
|
||||
<UserIcon className="w-4 h-4" />
|
||||
{t('team.myProfile')}
|
||||
</button>
|
||||
|
||||
|
||||
{/* Create Team button (managers and superadmins only) */}
|
||||
{canManageTeam && (
|
||||
<button
|
||||
onClick={() => setPanelTeam({})}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white border border-border text-text-primary rounded-lg text-sm font-medium hover:bg-surface-secondary transition-colors"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
{t('teams.createTeam')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Add member button (managers and superadmins only) */}
|
||||
{canManageTeam && (
|
||||
<button
|
||||
@@ -281,168 +357,209 @@ export default function Team() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Member grid */}
|
||||
{teamMembers.length === 0 ? (
|
||||
<div className="py-20 text-center">
|
||||
<Users className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
|
||||
<p className="text-text-secondary font-medium">{t('team.noMembers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 stagger-children">
|
||||
{teamMembers.map(member => (
|
||||
<MemberCard key={member._id} member={member} onClick={openMemberDetail} />
|
||||
))}
|
||||
</div>
|
||||
{/* Grid view: team filter pills + member cards */}
|
||||
{viewMode === 'grid' && (
|
||||
<>
|
||||
{/* Team filter pills */}
|
||||
{teams.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium text-text-tertiary">{t('teams.teams')}:</span>
|
||||
<button
|
||||
onClick={() => setTeamFilter(null)}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border font-medium transition-colors ${
|
||||
!teamFilter ? 'bg-brand-primary text-white border-brand-primary' : 'bg-white text-text-secondary border-border hover:bg-surface-tertiary'
|
||||
}`}
|
||||
>
|
||||
{t('common.all')}
|
||||
</button>
|
||||
{teams.map(team => {
|
||||
const tid = team.id || team._id
|
||||
const active = teamFilter === tid
|
||||
return (
|
||||
<div key={tid} className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => setTeamFilter(active ? null : tid)}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border font-medium transition-colors ${
|
||||
active ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-text-secondary border-border hover:bg-surface-tertiary'
|
||||
}`}
|
||||
>
|
||||
{team.name} ({team.member_count || 0})
|
||||
</button>
|
||||
{canManageTeam && (
|
||||
<button
|
||||
onClick={() => setPanelTeam(team)}
|
||||
className="p-1 text-text-tertiary hover:text-text-primary rounded"
|
||||
title={t('teams.editTeam')}
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Member grid */}
|
||||
{displayedMembers.length === 0 ? (
|
||||
<div className="py-20 text-center">
|
||||
<Users className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
|
||||
<p className="text-text-secondary font-medium">{t('team.noMembers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 stagger-children">
|
||||
{displayedMembers.map(member => (
|
||||
<MemberCard key={member._id} member={member} onClick={openMemberDetail} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onClose={() => { setShowModal(false); setEditingMember(null); setIsEditingSelf(false) }}
|
||||
title={isEditingSelf ? t('team.editProfile') : (editingMember ? t('team.editMember') : t('team.newMember'))}
|
||||
size="md"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.name')} *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={e => setFormData(f => ({ ...f, name: 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.fullName')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isEditingSelf && (
|
||||
{/* Teams (org chart) view */}
|
||||
{viewMode === 'teams' && (
|
||||
<div className="space-y-6">
|
||||
{teams.length === 0 && unassignedMembers.length === 0 ? (
|
||||
<div className="py-20 text-center">
|
||||
<Users className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
|
||||
<p className="text-text-secondary font-medium">{t('team.noMembers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.email')} *</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={e => setFormData(f => ({ ...f, email: 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="email@example.com"
|
||||
disabled={editingMember}
|
||||
/>
|
||||
</div>
|
||||
{teams.map(team => {
|
||||
const tid = team.id || team._id
|
||||
const members = teamMembers.filter(m => m.teams?.some(t => t.id === tid))
|
||||
return (
|
||||
<div key={tid} className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
{/* Team header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-600 flex items-center justify-center text-white">
|
||||
<Users className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-text-primary">{team.name}</h3>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{members.length} {members.length !== 1 ? t('team.membersPlural') : t('team.member')}
|
||||
{team.description && ` · ${team.description}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{canManageTeam && (
|
||||
<button
|
||||
onClick={() => setPanelTeam(team)}
|
||||
className="px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!editingMember && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.password')} {editingMember && t('team.optional')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={e => setFormData(f => ({ ...f, password: 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="••••••••"
|
||||
/>
|
||||
{!formData.password && !editingMember && (
|
||||
<p className="text-xs text-text-tertiary mt-1">{t('team.defaultPassword')}</p>
|
||||
)}
|
||||
{/* Team members */}
|
||||
{members.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-text-tertiary">{t('team.noMembers')}</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border-light">
|
||||
{members.map(member => {
|
||||
const colorIndex = (member.name?.charCodeAt(0) || 0) % avatarColors.length
|
||||
return (
|
||||
<div
|
||||
key={member._id}
|
||||
onClick={() => openMemberDetail(member)}
|
||||
className="flex items-center gap-4 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-full bg-gradient-to-br ${avatarColors[colorIndex]} flex items-center justify-center text-white text-sm font-bold shrink-0`}>
|
||||
{getInitials(member.name)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary">{member.name}</p>
|
||||
<p className="text-xs text-text-tertiary capitalize">{(member.team_role || member.role)?.replace('_', ' ')}</p>
|
||||
</div>
|
||||
{member.brands && member.brands.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 shrink-0">
|
||||
{member.brands.slice(0, 3).map(b => <BrandBadge key={b} brand={b} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Unassigned members */}
|
||||
{unassignedMembers.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-5 py-4 bg-gray-50 border-b border-border">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-400 flex items-center justify-center text-white">
|
||||
<UserIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-text-primary">{t('team.unassigned')}</h3>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{unassignedMembers.length} {unassignedMembers.length !== 1 ? t('team.membersPlural') : t('team.member')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-border-light">
|
||||
{unassignedMembers.map(member => {
|
||||
const colorIndex = (member.name?.charCodeAt(0) || 0) % avatarColors.length
|
||||
return (
|
||||
<div
|
||||
key={member._id}
|
||||
onClick={() => openMemberDetail(member)}
|
||||
className="flex items-center gap-4 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-full bg-gradient-to-br ${avatarColors[colorIndex]} flex items-center justify-center text-white text-sm font-bold shrink-0`}>
|
||||
{getInitials(member.name)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary">{member.name}</p>
|
||||
<p className="text-xs text-text-tertiary capitalize">{(member.team_role || member.role)?.replace('_', ' ')}</p>
|
||||
</div>
|
||||
{member.brands && member.brands.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 shrink-0">
|
||||
{member.brands.slice(0, 3).map(b => <BrandBadge key={b} brand={b} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.teamRole')}</label>
|
||||
{user?.role === 'manager' && !editingMember && !isEditingSelf ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value="Contributor"
|
||||
disabled
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface-tertiary text-text-tertiary cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary mt-1">{t('team.fixedRole')}</p>
|
||||
</>
|
||||
) : (
|
||||
<select
|
||||
value={formData.role}
|
||||
onChange={e => setFormData(f => ({ ...f, 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"
|
||||
>
|
||||
{ROLES.map(r => <option key={r.value} value={r.value}>{r.label}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.phone')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.phone}
|
||||
onChange={e => setFormData(f => ({ ...f, phone: 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="+966 ..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.brands')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.brands}
|
||||
onChange={e => setFormData(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="Brand A, Brand B"
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary mt-1">{t('team.brandsHelp')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
|
||||
{editingMember && !isEditingSelf && canManageTeam && (
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto"
|
||||
>
|
||||
{t('team.remove')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setShowModal(false); setEditingMember(null); setIsEditingSelf(false) }}
|
||||
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!formData.name || (!isEditingSelf && !editingMember && !formData.email)}
|
||||
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"
|
||||
>
|
||||
{isEditingSelf ? t('team.saveProfile') : (editingMember ? t('team.saveChanges') : t('team.addMember'))}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<Modal
|
||||
isOpen={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
title={t('team.removeMember')}
|
||||
isConfirm
|
||||
danger
|
||||
confirmText={t('team.remove')}
|
||||
onConfirm={async () => {
|
||||
if (editingMember) {
|
||||
await api.delete(`/users/team/${editingMember._id}`)
|
||||
setShowModal(false)
|
||||
setEditingMember(null)
|
||||
setIsEditingSelf(false)
|
||||
setShowDeleteConfirm(false)
|
||||
if (selectedMember?._id === editingMember._id) {
|
||||
setSelectedMember(null)
|
||||
}
|
||||
loadTeam()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('team.removeConfirm', { name: editingMember?.name })}
|
||||
</Modal>
|
||||
{/* Team Member Panel */}
|
||||
{panelMember && (
|
||||
<TeamMemberPanel
|
||||
member={panelMember}
|
||||
isEditingSelf={panelIsEditingSelf}
|
||||
onClose={() => setPanelMember(null)}
|
||||
onSave={handlePanelSave}
|
||||
onDelete={canManageTeam ? handlePanelDelete : null}
|
||||
canManageTeam={canManageTeam}
|
||||
userRole={user?.role}
|
||||
teams={teams}
|
||||
brands={brands}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Team Panel */}
|
||||
{panelTeam && (
|
||||
<TeamPanel
|
||||
team={panelTeam}
|
||||
onClose={() => setPanelTeam(null)}
|
||||
onSave={handleTeamSave}
|
||||
onDelete={canManageTeam ? handleTeamDelete : null}
|
||||
teamMembers={teamMembers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user