feat: hide dashboard sections for modules the user cannot access
All checks were successful
Deploy / deploy (push) Successful in 11s
All checks were successful
Deploy / deploy (push) Successful in 11s
Only fetch data and render stat cards, lists, and widgets for modules the user has enabled (marketing, projects, finance). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { format, isAfter, isBefore, addDays } from 'date-fns'
|
import { format, isAfter, isBefore, addDays } from 'date-fns'
|
||||||
import { FileText, Megaphone, AlertTriangle, ArrowRight, Clock, Wallet, TrendingUp, TrendingDown, DollarSign, Landmark, CheckSquare, FolderKanban } from 'lucide-react'
|
import { FileText, Megaphone, AlertTriangle, ArrowRight, Clock, Wallet, TrendingUp, TrendingDown, DollarSign, Landmark, CheckSquare, FolderKanban } from 'lucide-react'
|
||||||
import { AppContext } from '../App'
|
import { AppContext } from '../App'
|
||||||
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { useLanguage } from '../i18n/LanguageContext'
|
import { useLanguage } from '../i18n/LanguageContext'
|
||||||
import { api, PRIORITY_CONFIG } from '../utils/api'
|
import { api, PRIORITY_CONFIG } from '../utils/api'
|
||||||
import StatCard from '../components/StatCard'
|
import StatCard from '../components/StatCard'
|
||||||
@@ -264,6 +265,7 @@ export default function Dashboard() {
|
|||||||
const { t, currencySymbol } = useLanguage()
|
const { t, currencySymbol } = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { currentUser, teamMembers } = useContext(AppContext)
|
const { currentUser, teamMembers } = useContext(AppContext)
|
||||||
|
const { hasModule } = useAuth()
|
||||||
const [posts, setPosts] = useState([])
|
const [posts, setPosts] = useState([])
|
||||||
const [campaigns, setCampaigns] = useState([])
|
const [campaigns, setCampaigns] = useState([])
|
||||||
const [tasks, setTasks] = useState([])
|
const [tasks, setTasks] = useState([])
|
||||||
@@ -282,18 +284,30 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [postsRes, campaignsRes, tasksRes, financeRes, projectsRes] = await Promise.allSettled([
|
const fetches = []
|
||||||
api.get('/posts?limit=50&sort=-createdAt'),
|
// Only fetch data for modules the user has access to
|
||||||
api.get('/campaigns'),
|
if (hasModule('marketing')) {
|
||||||
api.get('/tasks'),
|
fetches.push(api.get('/posts?limit=50&sort=-createdAt').then(r => ({ key: 'posts', data: r.data || r || [] })))
|
||||||
api.get('/finance/summary'),
|
fetches.push(api.get('/campaigns').then(r => ({ key: 'campaigns', data: r.data || r || [] })))
|
||||||
api.get('/projects'),
|
}
|
||||||
])
|
if (hasModule('projects')) {
|
||||||
setPosts(postsRes.status === 'fulfilled' ? (postsRes.value.data || postsRes.value || []) : [])
|
fetches.push(api.get('/tasks').then(r => ({ key: 'tasks', data: r.data || r || [] })))
|
||||||
setCampaigns(campaignsRes.status === 'fulfilled' ? (campaignsRes.value.data || campaignsRes.value || []) : [])
|
fetches.push(api.get('/projects').then(r => ({ key: 'projects', data: r.data || r || [] })))
|
||||||
setTasks(tasksRes.status === 'fulfilled' ? (tasksRes.value.data || tasksRes.value || []) : [])
|
}
|
||||||
setFinance(financeRes.status === 'fulfilled' ? (financeRes.value.data || financeRes.value || null) : null)
|
if (hasModule('finance')) {
|
||||||
setProjects(projectsRes.status === 'fulfilled' ? (projectsRes.value.data || projectsRes.value || []) : [])
|
fetches.push(api.get('/finance/summary').then(r => ({ key: 'finance', data: r.data || r || null })))
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(fetches)
|
||||||
|
results.forEach(r => {
|
||||||
|
if (r.status !== 'fulfilled') return
|
||||||
|
const { key, data } = r.value
|
||||||
|
if (key === 'posts') setPosts(data)
|
||||||
|
else if (key === 'campaigns') setCampaigns(data)
|
||||||
|
else if (key === 'tasks') setTasks(data)
|
||||||
|
else if (key === 'projects') setProjects(data)
|
||||||
|
else if (key === 'finance') setFinance(data)
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Dashboard load error:', err)
|
console.error('Dashboard load error:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -339,6 +353,42 @@ export default function Dashboard() {
|
|||||||
.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate))
|
.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate))
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
|
|
||||||
|
const statCards = []
|
||||||
|
if (hasModule('marketing')) {
|
||||||
|
statCards.push({
|
||||||
|
icon: FileText,
|
||||||
|
label: t('dashboard.totalPosts'),
|
||||||
|
value: filteredPosts.length || 0,
|
||||||
|
subtitle: `${filteredPosts.filter(p => p.status === 'published').length} ${t('dashboard.published')}`,
|
||||||
|
color: 'brand-primary',
|
||||||
|
})
|
||||||
|
statCards.push({
|
||||||
|
icon: Megaphone,
|
||||||
|
label: t('dashboard.activeCampaigns'),
|
||||||
|
value: activeCampaigns,
|
||||||
|
subtitle: `${campaigns.length} ${t('dashboard.total')}`,
|
||||||
|
color: 'brand-secondary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hasModule('finance')) {
|
||||||
|
statCards.push({
|
||||||
|
icon: Landmark,
|
||||||
|
label: t('dashboard.budgetRemaining'),
|
||||||
|
value: `${(finance?.remaining ?? 0).toLocaleString()}`,
|
||||||
|
subtitle: finance?.totalReceived ? `${(finance.spent || 0).toLocaleString()} ${t('dashboard.spent')} ${t('dashboard.of')} ${finance.totalReceived.toLocaleString()} ${currencySymbol}` : t('dashboard.noBudget'),
|
||||||
|
color: 'brand-tertiary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hasModule('projects')) {
|
||||||
|
statCards.push({
|
||||||
|
icon: AlertTriangle,
|
||||||
|
label: t('dashboard.overdueTasks'),
|
||||||
|
value: overdueTasks,
|
||||||
|
subtitle: overdueTasks > 0 ? t('dashboard.needsAttention') : t('dashboard.allOnTrack'),
|
||||||
|
color: 'brand-quaternary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <SkeletonDashboard />
|
return <SkeletonDashboard />
|
||||||
}
|
}
|
||||||
@@ -363,121 +413,110 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
|
{statCards.length > 0 && (
|
||||||
<StatCard
|
<div className={`grid grid-cols-1 sm:grid-cols-2 ${statCards.length >= 4 ? 'lg:grid-cols-4' : statCards.length === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-2'} gap-4 stagger-children`}>
|
||||||
icon={FileText}
|
{statCards.map((card, i) => (
|
||||||
label={t('dashboard.totalPosts')}
|
<StatCard key={i} {...card} />
|
||||||
value={filteredPosts.length || 0}
|
))}
|
||||||
subtitle={`${filteredPosts.filter(p => p.status === 'published').length} ${t('dashboard.published')}`}
|
</div>
|
||||||
color="brand-primary"
|
)}
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={Megaphone}
|
|
||||||
label={t('dashboard.activeCampaigns')}
|
|
||||||
value={activeCampaigns}
|
|
||||||
subtitle={`${campaigns.length} ${t('dashboard.total')}`}
|
|
||||||
color="brand-secondary"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={Landmark}
|
|
||||||
label={t('dashboard.budgetRemaining')}
|
|
||||||
value={`${(finance?.remaining ?? 0).toLocaleString()}`}
|
|
||||||
subtitle={finance?.totalReceived ? `${(finance.spent || 0).toLocaleString()} ${t('dashboard.spent')} ${t('dashboard.of')} ${finance.totalReceived.toLocaleString()} ${currencySymbol}` : t('dashboard.noBudget')}
|
|
||||||
color="brand-tertiary"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={AlertTriangle}
|
|
||||||
label={t('dashboard.overdueTasks')}
|
|
||||||
value={overdueTasks}
|
|
||||||
subtitle={overdueTasks > 0 ? t('dashboard.needsAttention') : t('dashboard.allOnTrack')}
|
|
||||||
color="brand-quaternary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* My Tasks + Project Progress */}
|
{/* My Tasks + Project Progress */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
{hasModule('projects') && (
|
||||||
<MyTasksList tasks={filteredTasks} currentUserId={currentUser?.id || currentUser?._id} navigate={navigate} t={t} />
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<ProjectProgress projects={projects} tasks={tasks} t={t} />
|
<MyTasksList tasks={filteredTasks} currentUserId={currentUser?.id || currentUser?._id} navigate={navigate} t={t} />
|
||||||
</div>
|
<ProjectProgress projects={projects} tasks={tasks} t={t} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Budget + Active Campaigns */}
|
{/* Budget + Active Campaigns */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
{(hasModule('finance') || hasModule('marketing')) && (
|
||||||
<FinanceMini finance={finance} />
|
<div className={`grid grid-cols-1 ${hasModule('finance') && hasModule('marketing') ? 'lg:grid-cols-3' : ''} gap-6`}>
|
||||||
<div className="lg:col-span-2">
|
{hasModule('finance') && <FinanceMini finance={finance} />}
|
||||||
<ActiveCampaignsList campaigns={campaigns} finance={finance} />
|
{hasModule('marketing') && (
|
||||||
|
<div className={hasModule('finance') ? 'lg:col-span-2' : ''}>
|
||||||
|
<ActiveCampaignsList campaigns={campaigns} finance={finance} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Recent Posts + Upcoming Deadlines */}
|
{/* Recent Posts + Upcoming Deadlines */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
{(hasModule('marketing') || hasModule('projects')) && (
|
||||||
{/* Recent Posts */}
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="section-card">
|
{/* Recent Posts */}
|
||||||
<div className="section-card-header flex items-center justify-between">
|
{hasModule('marketing') && (
|
||||||
<h3 className="font-semibold text-text-primary">{t('dashboard.recentPosts')}</h3>
|
<div className="section-card">
|
||||||
<Link to="/posts" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
<div className="section-card-header flex items-center justify-between">
|
||||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
<h3 className="font-semibold text-text-primary">{t('dashboard.recentPosts')}</h3>
|
||||||
</Link>
|
<Link to="/posts" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||||
</div>
|
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||||
<div className="divide-y divide-border-light">
|
</Link>
|
||||||
{filteredPosts.length === 0 ? (
|
|
||||||
<div className="py-12 text-center text-sm text-text-tertiary">
|
|
||||||
{t('dashboard.noPostsYet')}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="divide-y divide-border-light">
|
||||||
filteredPosts.slice(0, 8).map((post) => (
|
{filteredPosts.length === 0 ? (
|
||||||
<div
|
<div className="py-12 text-center text-sm text-text-tertiary">
|
||||||
key={post._id}
|
{t('dashboard.noPostsYet')}
|
||||||
onClick={() => navigate('/posts')}
|
</div>
|
||||||
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
) : (
|
||||||
>
|
filteredPosts.slice(0, 8).map((post) => (
|
||||||
<div className="flex-1 min-w-0">
|
<div
|
||||||
<p className="text-sm font-medium text-text-primary truncate">{post.title}</p>
|
key={post._id}
|
||||||
<div className="flex items-center gap-2 mt-1">
|
onClick={() => navigate('/posts')}
|
||||||
{post.brand && <BrandBadge brand={post.brand} />}
|
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-text-primary truncate">{post.title}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
{post.brand && <BrandBadge brand={post.brand} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={post.status} size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
<StatusBadge status={post.status} size="xs" />
|
)}
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upcoming Deadlines */}
|
|
||||||
<div className="section-card">
|
|
||||||
<div className="section-card-header flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold text-text-primary">{t('dashboard.upcomingDeadlines')}</h3>
|
|
||||||
<Link to="/tasks" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
|
||||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-border-light">
|
|
||||||
{upcomingDeadlines.length === 0 ? (
|
|
||||||
<div className="py-12 text-center text-sm text-text-tertiary">
|
|
||||||
{t('dashboard.noUpcomingDeadlines')}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
upcomingDeadlines.map((task) => (
|
)}
|
||||||
<div
|
|
||||||
key={task._id}
|
{/* Upcoming Deadlines */}
|
||||||
onClick={() => navigate('/tasks')}
|
{hasModule('projects') && (
|
||||||
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
<div className="section-card">
|
||||||
>
|
<div className="section-card-header flex items-center justify-between">
|
||||||
<div className={`w-2 h-2 rounded-full ${(PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium).color}`} />
|
<h3 className="font-semibold text-text-primary">{t('dashboard.upcomingDeadlines')}</h3>
|
||||||
<div className="flex-1 min-w-0">
|
<Link to="/tasks" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||||
<p className="text-sm font-medium text-text-primary truncate">{task.title}</p>
|
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||||
<StatusBadge status={task.status} size="xs" />
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border-light">
|
||||||
|
{upcomingDeadlines.length === 0 ? (
|
||||||
|
<div className="py-12 text-center text-sm text-text-tertiary">
|
||||||
|
{t('dashboard.noUpcomingDeadlines')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-xs text-text-tertiary shrink-0">
|
) : (
|
||||||
<Clock className="w-3.5 h-3.5" />
|
upcomingDeadlines.map((task) => (
|
||||||
{format(new Date(task.dueDate), 'MMM d')}
|
<div
|
||||||
</div>
|
key={task._id}
|
||||||
</div>
|
onClick={() => navigate('/tasks')}
|
||||||
))
|
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||||
)}
|
>
|
||||||
</div>
|
<div className={`w-2 h-2 rounded-full ${(PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium).color}`} />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-text-primary truncate">{task.title}</p>
|
||||||
|
<StatusBadge status={task.status} size="xs" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-text-tertiary shrink-0">
|
||||||
|
<Clock className="w-3.5 h-3.5" />
|
||||||
|
{format(new Date(task.dueDate), 'MMM d')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user