@@ -190,10 +197,10 @@ export default function Dashboard() {
{/* Welcome */}
@@ -245,15 +252,15 @@ export default function Dashboard() {
{/* Recent Posts */}
-
Recent Posts
+
{t('dashboard.recentPosts')}
- View all
+ {t('dashboard.viewAll')}
{posts.length === 0 ? (
- No posts yet. Create your first post!
+ {t('dashboard.noPostsYet')}
) : (
posts.slice(0, 8).map((post) => (
@@ -274,24 +281,20 @@ export default function Dashboard() {
{/* Upcoming Deadlines */}
-
Upcoming Deadlines
+
{t('dashboard.upcomingDeadlines')}
- View all
+ {t('dashboard.viewAll')}
{upcomingDeadlines.length === 0 ? (
- No upcoming deadlines this week. 🎉
+ {t('dashboard.noUpcomingDeadlines')}
) : (
upcomingDeadlines.map((task) => (
-
+
{task.title}
diff --git a/client/src/pages/Finance.jsx b/client/src/pages/Finance.jsx
index 818191e..32991e4 100644
--- a/client/src/pages/Finance.jsx
+++ b/client/src/pages/Finance.jsx
@@ -18,7 +18,7 @@ const EMPTY_ENTRY = {
label: '', amount: '', source: '', campaign_id: '', category: 'marketing', date_received: new Date().toISOString().slice(0, 10), notes: '',
}
-function StatCard({ icon: Icon, label, value, sub, color = 'text-text-primary', bgColor = 'bg-white' }) {
+function FinanceStatCard({ icon: Icon, label, value, sub, color = 'text-text-primary', bgColor = 'bg-white' }) {
return (
@@ -154,11 +154,11 @@ export default function Finance() {
{/* Top metrics */}
-
-
- = 0 ? 'text-emerald-600' : 'text-red-600'} />
-
- = 0 ? TrendingUp : TrendingDown} label="Global ROI"
+
+
+ = 0 ? 'text-emerald-600' : 'text-red-600'} />
+
+ = 0 ? TrendingUp : TrendingDown} label="Global ROI"
value={`${roi.toFixed(1)}%`}
color={roi >= 0 ? 'text-emerald-600' : 'text-red-600'} />
diff --git a/client/src/pages/PostProduction.jsx b/client/src/pages/PostProduction.jsx
index 768c99a..890bc04 100644
--- a/client/src/pages/PostProduction.jsx
+++ b/client/src/pages/PostProduction.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useContext, useRef } from 'react'
-import { Plus, LayoutGrid, List, Search, Filter, Upload, X, Paperclip, Link2, FileText, Image as ImageIcon, Trash2, ExternalLink } from 'lucide-react'
+import { Plus, LayoutGrid, List, Search, Filter, Upload, X, Paperclip, Link2, FileText, Image as ImageIcon, Trash2, ExternalLink, FolderOpen } from 'lucide-react'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext'
@@ -8,6 +8,7 @@ import KanbanBoard from '../components/KanbanBoard'
import PostCard from '../components/PostCard'
import StatusBadge from '../components/StatusBadge'
import Modal from '../components/Modal'
+import CommentsSection from '../components/CommentsSection'
const EMPTY_POST = {
title: '', description: '', brand_id: '', platforms: [],
@@ -33,7 +34,11 @@ export default function PostProduction() {
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
const [publishError, setPublishError] = useState('')
+ const [moveError, setMoveError] = useState('')
const [dragActive, setDragActive] = useState(false)
+ const [showAssetPicker, setShowAssetPicker] = useState(false)
+ const [availableAssets, setAvailableAssets] = useState([])
+ const [assetSearch, setAssetSearch] = useState('')
const fileInputRef = useRef(null)
useEffect(() => {
@@ -106,7 +111,8 @@ export default function PostProduction() {
} catch (err) {
console.error('Move failed:', err)
if (err.message?.includes('Cannot publish')) {
- alert('Cannot publish: all platform publication links must be filled first.')
+ setMoveError(t('posts.publishRequired'))
+ setTimeout(() => setMoveError(''), 5000)
}
}
}
@@ -152,6 +158,30 @@ export default function PostProduction() {
}
}
+ const openAssetPicker = async () => {
+ try {
+ const data = await api.get('/assets')
+ setAvailableAssets(Array.isArray(data) ? data : (data.data || []))
+ } catch (err) {
+ console.error('Failed to load assets:', err)
+ setAvailableAssets([])
+ }
+ setAssetSearch('')
+ setShowAssetPicker(true)
+ }
+
+ const handleAttachAsset = async (assetId) => {
+ if (!editingPost) return
+ const postId = editingPost._id || editingPost.id
+ try {
+ await api.post(`/posts/${postId}/attachments/from-asset`, { asset_id: assetId })
+ loadAttachments(postId)
+ setShowAssetPicker(false)
+ } catch (err) {
+ console.error('Attach asset failed:', err)
+ }
+ }
+
const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(true) }
const handleDragLeaveZone = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(false) }
const handleDragOverZone = (e) => { e.preventDefault(); e.stopPropagation() }
@@ -297,6 +327,16 @@ export default function PostProduction() {
+ {/* Move error banner */}
+ {moveError && (
+
+ {moveError}
+
+
+ )}
+
{/* Content */}
{view === 'kanban' ? (
@@ -534,27 +574,29 @@ export default function PostProduction() {
const name = att.original_name || att.originalName || att.filename
return (
- {isImage ? (
-
-
-
- ) : (
-
-
- {name}
-
- )}
-
+
+ {isImage ? (
+
+
+
+ ) : (
+
+
+ {name}
+
+ )}
+
+
{name}
@@ -589,6 +631,65 @@ export default function PostProduction() {
{t('posts.maxSize')}
+ {/* Attach from Assets button */}
+
+
+ {/* Asset picker */}
+ {showAssetPicker && (
+
+
+
{t('posts.selectAssets')}
+
+
+
setAssetSearch(e.target.value)}
+ placeholder={t('common.search')}
+ className="w-full px-3 py-1.5 text-xs border border-border rounded-lg mb-2 focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
+ />
+
+ {availableAssets
+ .filter(a => !assetSearch || (a.original_name || a.filename || '').toLowerCase().includes(assetSearch.toLowerCase()))
+ .map(asset => {
+ const isImage = asset.mime_type?.startsWith('image/')
+ const assetUrl = `/api/uploads/${asset.filename}`
+ const name = asset.original_name || asset.filename
+ return (
+
+ )
+ })}
+
+ {availableAssets.filter(a => !assetSearch || (a.original_name || a.filename || '').toLowerCase().includes(assetSearch.toLowerCase())).length === 0 && (
+
{t('posts.noAssetsFound')}
+ )}
+
+ )}
+
{/* Upload progress */}
{uploading && (
@@ -607,6 +708,11 @@ export default function PostProduction() {
)}
+ {/* Comments (only for existing posts) */}
+ {editingPost && (
+
+ )}
+
{/* Publish validation error */}
{publishError && (
diff --git a/client/src/pages/ProjectDetail.jsx b/client/src/pages/ProjectDetail.jsx
index 41615b9..039fb69 100644
--- a/client/src/pages/ProjectDetail.jsx
+++ b/client/src/pages/ProjectDetail.jsx
@@ -4,12 +4,13 @@ import {
ArrowLeft, Plus, Check, Trash2, Edit3, LayoutGrid, List,
GanttChart, Settings, Calendar, Clock
} from 'lucide-react'
-import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore, parseISO } from 'date-fns'
+import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore } from 'date-fns'
import { AppContext } from '../App'
import { api, PRIORITY_CONFIG } from '../utils/api'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
+import CommentsSection from '../components/CommentsSection'
const TASK_COLUMNS = [
{ id: 'todo', label: 'To Do', color: 'bg-gray-400' },
@@ -24,6 +25,7 @@ export default function ProjectDetail() {
const [project, setProject] = useState(null)
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
+ const [assignableUsers, setAssignableUsers] = useState([])
const [view, setView] = useState('kanban')
const [showTaskModal, setShowTaskModal] = useState(false)
const [showProjectModal, setShowProjectModal] = useState(false)
@@ -42,6 +44,9 @@ export default function ProjectDetail() {
const [dragOverCol, setDragOverCol] = useState(null)
useEffect(() => { loadProject() }, [id])
+ useEffect(() => {
+ api.get('/users/assignable').then(res => setAssignableUsers(res.data || res || [])).catch(() => {})
+ }, [])
const loadProject = async () => {
try {
@@ -208,31 +213,6 @@ export default function ProjectDetail() {
const ownerName = project.ownerName || project.owner_name
const brandName = project.brandName || project.brand_name
- // Gantt chart helpers
- const getGanttRange = () => {
- const today = startOfDay(new Date())
- let earliest = today
- let latest = addDays(today, 14)
-
- tasks.forEach(t => {
- if (t.createdAt) {
- const d = startOfDay(new Date(t.createdAt))
- if (isBefore(d, earliest)) earliest = d
- }
- if (t.dueDate) {
- const d = startOfDay(new Date(t.dueDate))
- if (isAfter(d, latest)) latest = addDays(d, 1)
- }
- })
- if (project.dueDate) {
- const d = startOfDay(new Date(project.dueDate))
- if (isAfter(d, latest)) latest = addDays(d, 1)
- }
- // Ensure minimum 14 days
- if (differenceInDays(latest, earliest) < 14) latest = addDays(earliest, 14)
- return { earliest, latest, totalDays: differenceInDays(latest, earliest) + 1 }
- }
-
return (
{/* Back button */}
@@ -296,6 +276,11 @@ export default function ProjectDetail() {
+ {/* Discussion */}
+
+
+
+
{/* View switcher + Add Task */}
@@ -491,7 +476,7 @@ export default function ProjectDetail() {
@@ -586,6 +571,19 @@ export default function ProjectDetail() {
+
+ {/* ─── DELETE TASK CONFIRMATION ─── */}
+
{ setShowDeleteConfirm(false); setTaskToDelete(null) }}
+ title="Delete Task?"
+ isConfirm
+ danger
+ confirmText="Delete Task"
+ onConfirm={confirmDeleteTask}
+ >
+ Are you sure you want to delete this task? This action cannot be undone.
+
)
}
@@ -760,18 +758,6 @@ function GanttView({ tasks, project, onEditTask }) {
- {/* Delete Task Confirmation */}
-
{ setShowDeleteConfirm(false); setTaskToDelete(null) }}
- title="Delete Task?"
- isConfirm
- danger
- confirmText="Delete Task"
- onConfirm={confirmDeleteTask}
- >
- Are you sure you want to delete this task? This action cannot be undone.
-
)
}
diff --git a/client/src/pages/Tasks.jsx b/client/src/pages/Tasks.jsx
index d46b89b..fafb8ea 100644
--- a/client/src/pages/Tasks.jsx
+++ b/client/src/pages/Tasks.jsx
@@ -6,10 +6,11 @@ import { useLanguage } from '../i18n/LanguageContext'
import { api } from '../utils/api'
import TaskCard from '../components/TaskCard'
import Modal from '../components/Modal'
+import CommentsSection from '../components/CommentsSection'
export default function Tasks() {
const { t } = useLanguage()
- const { currentUser, teamMembers } = useContext(AppContext)
+ const { currentUser } = useContext(AppContext)
const { user: authUser, canEditResource, canDeleteResource } = useAuth()
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
@@ -21,6 +22,7 @@ export default function Tasks() {
const [taskToDelete, setTaskToDelete] = useState(null)
const [filterView, setFilterView] = useState('all') // 'all' | 'assigned_to_me' | 'created_by_me' | member id
const [users, setUsers] = useState([]) // for superadmin member filter
+ const [assignableUsers, setAssignableUsers] = useState([])
const [formData, setFormData] = useState({
title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: ''
})
@@ -29,6 +31,8 @@ export default function Tasks() {
useEffect(() => { loadTasks() }, [currentUser])
useEffect(() => {
+ // Load assignable users for the assignment dropdown
+ api.get('/users/assignable').then(res => setAssignableUsers(res.data || res || [])).catch(() => {})
if (isSuperadmin) {
// Load team members for superadmin filter
api.get('/team').then(res => setUsers(res.data || res || [])).catch(() => {})
@@ -357,8 +361,8 @@ export default function Tasks() {
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"
>
- {(teamMembers || []).map(m => (
-
+ {(assignableUsers || []).map(m => (
+
))}
@@ -388,6 +392,11 @@ export default function Tasks() {
+ {/* Comments (only for existing tasks) */}
+ {editingTask && (
+
+ )}
+