feat: use modals for creation across all pages + fix profile prompt
All checks were successful
Deploy / deploy (push) Successful in 11s
All checks were successful
Deploy / deploy (push) Successful in 11s
- Campaigns: add create modal (name, brand, team, dates, budget) - PostProduction: add create modal (title, brand, campaign, assignee), auto-opens detail panel after creation - Tasks: add create modal (title, project, priority, assignee), auto-opens detail panel after creation - Fix profileComplete check: use !!user.name instead of !!user.team_role in /api/auth/me (was always showing profile prompt since team_role is now deprecated in favor of role_id) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,14 @@ import BrandBadge from '../components/BrandBadge'
|
|||||||
import BudgetBar from '../components/BudgetBar'
|
import BudgetBar from '../components/BudgetBar'
|
||||||
import InteractiveTimeline from '../components/InteractiveTimeline'
|
import InteractiveTimeline from '../components/InteractiveTimeline'
|
||||||
import CampaignDetailPanel from '../components/CampaignDetailPanel'
|
import CampaignDetailPanel from '../components/CampaignDetailPanel'
|
||||||
|
import Modal from '../components/Modal'
|
||||||
import { SkeletonStatCard, SkeletonTable } from '../components/SkeletonLoader'
|
import { SkeletonStatCard, SkeletonTable } from '../components/SkeletonLoader'
|
||||||
|
|
||||||
|
const EMPTY_CAMPAIGN = {
|
||||||
|
name: '', description: '', brand_id: '', status: 'planning',
|
||||||
|
start_date: '', end_date: '', budget: '', team_id: '',
|
||||||
|
}
|
||||||
|
|
||||||
function ROIBadge({ revenue, spent }) {
|
function ROIBadge({ revenue, spent }) {
|
||||||
if (!spent || spent <= 0) return null
|
if (!spent || spent <= 0) return null
|
||||||
const roi = ((revenue - spent) / spent * 100).toFixed(0)
|
const roi = ((revenue - spent) / spent * 100).toFixed(0)
|
||||||
@@ -36,14 +42,17 @@ function MetricCard({ icon: Icon, label, value, color = 'text-text-primary' }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Campaigns() {
|
export default function Campaigns() {
|
||||||
const { brands, getBrandName } = useContext(AppContext)
|
const { brands, getBrandName, teams } = useContext(AppContext)
|
||||||
const { lang, currencySymbol } = useLanguage()
|
const { t, lang, currencySymbol } = useLanguage()
|
||||||
const { permissions } = useAuth()
|
const { permissions } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [campaigns, setCampaigns] = useState([])
|
const [campaigns, setCampaigns] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [panelCampaign, setPanelCampaign] = useState(null)
|
const [panelCampaign, setPanelCampaign] = useState(null)
|
||||||
const [filters, setFilters] = useState({ brand: '', status: '' })
|
const [filters, setFilters] = useState({ brand: '', status: '' })
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [createForm, setCreateForm] = useState({ ...EMPTY_CAMPAIGN })
|
||||||
|
const [createSaving, setCreateSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => { loadCampaigns() }, [])
|
useEffect(() => { loadCampaigns() }, [])
|
||||||
|
|
||||||
@@ -73,7 +82,34 @@ export default function Campaigns() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openNew = () => {
|
const openNew = () => {
|
||||||
setPanelCampaign({ status: 'planning', platforms: [] })
|
setCreateForm({ ...EMPTY_CAMPAIGN })
|
||||||
|
setShowCreateModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setCreateSaving(true)
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
name: createForm.name,
|
||||||
|
description: createForm.description,
|
||||||
|
brand_id: createForm.brand_id ? Number(createForm.brand_id) : null,
|
||||||
|
status: createForm.status,
|
||||||
|
start_date: createForm.start_date || null,
|
||||||
|
end_date: createForm.end_date || null,
|
||||||
|
budget: createForm.budget ? Number(createForm.budget) : null,
|
||||||
|
team_id: createForm.team_id ? Number(createForm.team_id) : null,
|
||||||
|
}
|
||||||
|
const created = await api.post('/campaigns', data)
|
||||||
|
setShowCreateModal(false)
|
||||||
|
loadCampaigns()
|
||||||
|
// Navigate to the new campaign detail page
|
||||||
|
const id = created?.Id || created?.id || created?._id
|
||||||
|
if (id) navigate(`/campaigns/${id}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create campaign failed:', err)
|
||||||
|
} finally {
|
||||||
|
setCreateSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = campaigns.filter(c => {
|
const filtered = campaigns.filter(c => {
|
||||||
@@ -295,7 +331,62 @@ export default function Campaigns() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Campaign Panel */}
|
{/* Create Campaign Modal */}
|
||||||
|
<Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title={t('campaigns.newCampaign') || 'New Campaign'} size="md">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.name')} *</label>
|
||||||
|
<input type="text" value={createForm.name} onChange={e => setCreateForm(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" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.description')}</label>
|
||||||
|
<textarea value={createForm.description} onChange={e => setCreateForm(f => ({ ...f, description: e.target.value }))} rows={2}
|
||||||
|
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 resize-none" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.brand')}</label>
|
||||||
|
<select value={createForm.brand_id} onChange={e => setCreateForm(f => ({ ...f, brand_id: 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="">{t('posts.allBrands')}</option>
|
||||||
|
{brands.map(b => <option key={b.id || b._id} value={b.id || b._id}>{lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('common.team')}</label>
|
||||||
|
<select value={createForm.team_id} onChange={e => setCreateForm(f => ({ ...f, team_id: 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="">{t('common.noTeam')}</option>
|
||||||
|
{(teams || []).map(team => <option key={team.id || team._id} value={team.id || team._id}>{team.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.startDate')}</label>
|
||||||
|
<input type="date" value={createForm.start_date} onChange={e => setCreateForm(f => ({ ...f, start_date: 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.endDate')}</label>
|
||||||
|
<input type="date" value={createForm.end_date} onChange={e => setCreateForm(f => ({ ...f, end_date: 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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.budget')}</label>
|
||||||
|
<input type="number" value={createForm.budget} onChange={e => setCreateForm(f => ({ ...f, budget: 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="0" />
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCreate} disabled={!createForm.name || createSaving}
|
||||||
|
className={`w-full px-4 py-2.5 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 ${createSaving ? 'btn-loading' : ''}`}>
|
||||||
|
{t('campaigns.newCampaign') || 'Create Campaign'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Campaign Panel (edit only) */}
|
||||||
{panelCampaign && (
|
{panelCampaign && (
|
||||||
<CampaignDetailPanel
|
<CampaignDetailPanel
|
||||||
campaign={panelCampaign}
|
campaign={panelCampaign}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export default function PostProduction() {
|
|||||||
const [selectedIds, setSelectedIds] = useState(new Set())
|
const [selectedIds, setSelectedIds] = useState(new Set())
|
||||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [createForm, setCreateForm] = useState({ ...EMPTY_POST })
|
||||||
|
const [createSaving, setCreateSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPosts()
|
loadPosts()
|
||||||
@@ -133,7 +136,32 @@ export default function PostProduction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openNew = () => {
|
const openNew = () => {
|
||||||
setPanelPost(EMPTY_POST)
|
setCreateForm({ ...EMPTY_POST })
|
||||||
|
setShowCreateModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setCreateSaving(true)
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
title: createForm.title,
|
||||||
|
brand_id: createForm.brand_id ? Number(createForm.brand_id) : null,
|
||||||
|
campaign_id: createForm.campaign_id ? Number(createForm.campaign_id) : null,
|
||||||
|
assigned_to: createForm.assigned_to ? Number(createForm.assigned_to) : null,
|
||||||
|
status: 'draft',
|
||||||
|
}
|
||||||
|
const created = await api.post('/posts', data)
|
||||||
|
setShowCreateModal(false)
|
||||||
|
toast.success(t('posts.created'))
|
||||||
|
loadPosts()
|
||||||
|
// Open the detail panel for further editing
|
||||||
|
if (created) setPanelPost(created)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create post failed:', err)
|
||||||
|
toast.error(t('common.saveFailed'))
|
||||||
|
} finally {
|
||||||
|
setCreateSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredPosts = posts.filter(p => {
|
const filteredPosts = posts.filter(p => {
|
||||||
@@ -369,7 +397,48 @@ export default function PostProduction() {
|
|||||||
{t('common.bulkDeleteDesc')}
|
{t('common.bulkDeleteDesc')}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Post Detail Panel */}
|
{/* Create Post Modal */}
|
||||||
|
<Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title={t('posts.newPost')} size="md">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('posts.postTitle')} *</label>
|
||||||
|
<input type="text" value={createForm.title} onChange={e => setCreateForm(f => ({ ...f, title: 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" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('posts.brand')}</label>
|
||||||
|
<select value={createForm.brand_id} onChange={e => setCreateForm(f => ({ ...f, brand_id: 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="">{t('posts.allBrands')}</option>
|
||||||
|
{brands.map(b => <option key={b._id} value={b._id}>{lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('posts.campaign')}</label>
|
||||||
|
<select value={createForm.campaign_id} onChange={e => setCreateForm(f => ({ ...f, campaign_id: 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>
|
||||||
|
{campaigns.map(c => <option key={c._id || c.id} value={c._id || c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('posts.assignedTo')}</label>
|
||||||
|
<select value={createForm.assigned_to} onChange={e => setCreateForm(f => ({ ...f, assigned_to: 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="">{t('common.unassigned')}</option>
|
||||||
|
{teamMembers.map(m => <option key={m._id} value={String(m._id)}>{m.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCreate} disabled={!createForm.title || createSaving}
|
||||||
|
className={`w-full px-4 py-2.5 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 ${createSaving ? 'btn-loading' : ''}`}>
|
||||||
|
{t('posts.newPost')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Post Detail Panel (edit only) */}
|
||||||
{panelPost && (
|
{panelPost && (
|
||||||
<PostDetailPanel
|
<PostDetailPanel
|
||||||
post={panelPost}
|
post={panelPost}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export default function Tasks() {
|
|||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
const [selectedIds, setSelectedIds] = useState(new Set())
|
const [selectedIds, setSelectedIds] = useState(new Set())
|
||||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [createForm, setCreateForm] = useState({ title: '', project_id: '', brand_id: '', priority: 'medium', assigned_to: '' })
|
||||||
|
const [createSaving, setCreateSaving] = useState(false)
|
||||||
|
|
||||||
// Assignable users & team
|
// Assignable users & team
|
||||||
const [assignableUsers, setAssignableUsers] = useState([])
|
const [assignableUsers, setAssignableUsers] = useState([])
|
||||||
@@ -181,6 +184,32 @@ export default function Tasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCreateTask = async () => {
|
||||||
|
setCreateSaving(true)
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
title: createForm.title,
|
||||||
|
priority: createForm.priority,
|
||||||
|
status: 'todo',
|
||||||
|
project_id: createForm.project_id ? Number(createForm.project_id) : null,
|
||||||
|
brand_id: createForm.brand_id ? Number(createForm.brand_id) : null,
|
||||||
|
assigned_to: createForm.assigned_to ? Number(createForm.assigned_to) : null,
|
||||||
|
is_personal: false,
|
||||||
|
}
|
||||||
|
const created = await api.post('/tasks', data)
|
||||||
|
setShowCreateModal(false)
|
||||||
|
toast.success(t('tasks.created'))
|
||||||
|
loadTasks()
|
||||||
|
// Open detail panel for further editing
|
||||||
|
if (created) setSelectedTask(created)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create task failed:', err)
|
||||||
|
toast.error(t('common.saveFailed'))
|
||||||
|
} finally {
|
||||||
|
setCreateSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await api.post('/tasks/bulk-delete', { ids: [...selectedIds] })
|
await api.post('/tasks/bulk-delete', { ids: [...selectedIds] })
|
||||||
@@ -355,7 +384,7 @@ export default function Tasks() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSelectedTask({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '', project_id: '' }) }}
|
onClick={() => { setCreateForm({ title: '', project_id: '', brand_id: '', priority: 'medium', assigned_to: '' }); setShowCreateModal(true) }}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm shrink-0"
|
className="flex items-center gap-2 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm shrink-0"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@@ -689,6 +718,46 @@ export default function Tasks() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ─── Create Task Modal ──────────────────── */}
|
||||||
|
<Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title={t('tasks.newTask')} size="md">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('tasks.taskTitle')} *</label>
|
||||||
|
<input type="text" value={createForm.title} onChange={e => setCreateForm(f => ({ ...f, title: 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" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('tasks.project')}</label>
|
||||||
|
<select value={createForm.project_id} onChange={e => setCreateForm(f => ({ ...f, project_id: 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>
|
||||||
|
{projects.map(p => <option key={p._id || p.id} value={p._id || p.id}>{p.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('tasks.priority')}</label>
|
||||||
|
<select value={createForm.priority} onChange={e => setCreateForm(f => ({ ...f, priority: 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">
|
||||||
|
{Object.entries(PRIORITY_CONFIG).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('tasks.assignedTo')}</label>
|
||||||
|
<select value={createForm.assigned_to} onChange={e => setCreateForm(f => ({ ...f, assigned_to: 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="">{t('common.unassigned')}</option>
|
||||||
|
{assignableUsers.map(u => <option key={u._id || u.id} value={u._id || u.id}>{u.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCreateTask} disabled={!createForm.title || createSaving}
|
||||||
|
className={`w-full px-4 py-2.5 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 ${createSaving ? 'btn-loading' : ''}`}>
|
||||||
|
{t('tasks.newTask')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* ─── Bulk Delete Confirmation Modal ─────── */}
|
{/* ─── Bulk Delete Confirmation Modal ─────── */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showBulkDeleteConfirm}
|
isOpen={showBulkDeleteConfirm}
|
||||||
@@ -702,7 +771,7 @@ export default function Tasks() {
|
|||||||
{t('common.bulkDeleteDesc')}
|
{t('common.bulkDeleteDesc')}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* ─── Task Detail Side Panel ──────────────── */}
|
{/* ─── Task Detail Side Panel (edit only) ─── */}
|
||||||
{selectedTask && (
|
{selectedTask && (
|
||||||
<TaskDetailPanel
|
<TaskDetailPanel
|
||||||
task={selectedTask}
|
task={selectedTask}
|
||||||
|
|||||||
@@ -748,7 +748,7 @@ app.get('/api/auth/me', requireAuth, async (req, res) => {
|
|||||||
brands: user.brands, phone: user.phone,
|
brands: user.brands, phone: user.phone,
|
||||||
tutorial_completed: user.tutorial_completed,
|
tutorial_completed: user.tutorial_completed,
|
||||||
CreatedAt: user.CreatedAt, created_at: user.CreatedAt,
|
CreatedAt: user.CreatedAt, created_at: user.CreatedAt,
|
||||||
profileComplete: !!user.team_role,
|
profileComplete: !!user.name,
|
||||||
modules,
|
modules,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user