update on timeline on portfolio view + some corrections

This commit is contained in:
fahed
2026-02-10 13:20:49 +03:00
parent d15e54044e
commit 334727b232
37 changed files with 5119 additions and 1440 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useContext } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Plus, Edit2, Trash2, DollarSign, Eye, MousePointer, Target, TrendingUp, FileText, Megaphone, Search, Globe, Pencil, Users, X, UserPlus } from 'lucide-react'
import { ArrowLeft, Plus, Edit2, Trash2, DollarSign, Eye, MousePointer, Target, TrendingUp, FileText, Megaphone, Search, Globe, Pencil, Users, X, UserPlus, MessageCircle, Settings } from 'lucide-react'
import { format } from 'date-fns'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
@@ -66,6 +66,9 @@ export default function CampaignDetail() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [trackToDelete, setTrackToDelete] = useState(null)
const [selectedPost, setSelectedPost] = useState(null)
const [showDiscussion, setShowDiscussion] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [editForm, setEditForm] = useState({})
const isCreator = campaign?.createdByUserId === user?.id || campaign?.created_by_user_id === user?.id
const canManage = isSuperadmin || (permissions?.canEditCampaigns && isCreator)
@@ -202,6 +205,39 @@ export default function CampaignDetail() {
setShowTrackModal(true)
}
const openEditCampaign = () => {
setEditForm({
name: campaign.name || '',
description: campaign.description || '',
status: campaign.status || 'planning',
start_date: campaign.start_date ? new Date(campaign.start_date).toISOString().slice(0, 10) : '',
end_date: campaign.end_date ? new Date(campaign.end_date).toISOString().slice(0, 10) : '',
goals: campaign.goals || '',
platforms: campaign.platforms || [],
notes: campaign.notes || '',
})
setShowEditModal(true)
}
const saveCampaignEdit = async () => {
try {
await api.patch(`/campaigns/${id}`, {
name: editForm.name,
description: editForm.description,
status: editForm.status,
start_date: editForm.start_date,
end_date: editForm.end_date,
goals: editForm.goals,
platforms: editForm.platforms,
notes: editForm.notes,
})
setShowEditModal(false)
loadAll()
} catch (err) {
console.error('Failed to update campaign:', err)
}
}
const openMetrics = (track) => {
setMetricsTrack(track)
setMetricsForm({
@@ -236,37 +272,65 @@ export default function CampaignDetail() {
const totalRevenue = tracks.reduce((s, t) => s + (t.revenue || 0), 0)
return (
<div className="space-y-6 animate-fade-in">
<div className="flex gap-6 animate-fade-in">
{/* Main content */}
<div className={`space-y-6 min-w-0 ${showDiscussion ? 'flex-1' : 'w-full'}`}>
{/* Header */}
<div className="flex items-start gap-4">
<button onClick={() => navigate('/campaigns')} className="mt-1 p-1.5 hover:bg-surface-tertiary rounded-lg">
<ArrowLeft className="w-5 h-5 text-text-secondary" />
</button>
<div className="flex-1">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1">
<h1 className="text-xl font-bold text-text-primary">{campaign.name}</h1>
<StatusBadge status={campaign.status} />
{campaign.brand_name && <BrandBadge brand={campaign.brand_name} />}
</div>
{campaign.description && <p className="text-sm text-text-secondary">{campaign.description}</p>}
<div className="flex items-center gap-4 mt-2 text-xs text-text-tertiary">
<div className="flex items-center gap-3 mt-2 text-xs text-text-tertiary">
{campaign.start_date && campaign.end_date && (
<span>{format(new Date(campaign.start_date), 'MMM d')} {format(new Date(campaign.end_date), 'MMM d, yyyy')}</span>
)}
<span className="flex items-center gap-1">
<span>
Budget: {campaign.budget > 0 ? `${campaign.budget.toLocaleString()} SAR` : 'Not set'}
{canSetBudget && (
<button
onClick={() => { setBudgetValue(campaign.budget || ''); setEditingBudget(true) }}
className="p-0.5 rounded text-text-tertiary hover:text-brand-primary hover:bg-surface-tertiary"
title="Edit budget"
>
<Pencil className="w-3 h-3" />
</button>
)}
</span>
{campaign.platforms && campaign.platforms.length > 0 && (
<PlatformIcons platforms={campaign.platforms} size={16} />
)}
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => setShowDiscussion(prev => !prev)}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
showDiscussion
? 'bg-brand-primary text-white shadow-sm'
: 'bg-surface-tertiary text-text-secondary hover:bg-surface-tertiary/80 hover:text-text-primary'
}`}
>
<MessageCircle className="w-4 h-4" />
Discussion
</button>
{canSetBudget && (
<button
onClick={() => { setBudgetValue(campaign.budget || ''); setEditingBudget(true) }}
className="flex items-center gap-1.5 px-3 py-2 bg-surface-tertiary text-text-secondary hover:bg-surface-tertiary/80 hover:text-text-primary rounded-lg text-sm font-medium transition-colors"
>
<DollarSign className="w-4 h-4" />
Budget
</button>
)}
{canManage && (
<button
onClick={openEditCampaign}
className="flex items-center gap-1.5 px-3 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm transition-colors"
>
<Settings className="w-4 h-4" />
Edit
</button>
)}
</div>
</div>
{/* Assigned Team */}
@@ -480,10 +544,25 @@ export default function CampaignDetail() {
</div>
)}
{/* Discussion */}
<div className="bg-white rounded-xl border border-border p-6">
<CommentsSection entityType="campaign" entityId={Number(id)} />
</div>
</div>{/* end main content */}
{/* ─── DISCUSSION SIDEBAR ─── */}
{showDiscussion && (
<div className="w-[340px] shrink-0 bg-white rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-1.5">
<MessageCircle className="w-4 h-4" />
Discussion
</h3>
<button onClick={() => setShowDiscussion(false)} className="p-1 hover:bg-surface-tertiary rounded-lg text-text-tertiary">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<CommentsSection entityType="campaign" entityId={Number(id)} />
</div>
</div>
)}
{/* Add/Edit Track Modal */}
<Modal
@@ -756,6 +835,137 @@ export default function CampaignDetail() {
</div>
</Modal>
{/* Edit Campaign Modal */}
<Modal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
title="Edit Campaign"
size="lg"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Name *</label>
<input
type="text"
value={editForm.name || ''}
onChange={e => setEditForm(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"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
<textarea
value={editForm.description || ''}
onChange={e => setEditForm(f => ({ ...f, description: e.target.value }))}
rows={3}
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-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
<select
value={editForm.status || 'planning'}
onChange={e => setEditForm(f => ({ ...f, status: 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="planning">Planning</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Goals</label>
<input
type="text"
value={editForm.goals || ''}
onChange={e => setEditForm(f => ({ ...f, goals: 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 className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input
type="date"
value={editForm.start_date || ''}
onChange={e => setEditForm(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-sm font-medium text-text-primary mb-1">End Date</label>
<input
type="date"
value={editForm.end_date || ''}
onChange={e => setEditForm(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-sm font-medium text-text-primary mb-1">Platforms</label>
<div className="flex flex-wrap gap-2 p-2 border border-border rounded-lg bg-white min-h-[42px]">
{Object.entries(PLATFORMS).map(([k, v]) => {
const checked = (editForm.platforms || []).includes(k)
return (
<label
key={k}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-full cursor-pointer border transition-colors ${
checked
? 'bg-brand-primary/10 border-brand-primary/30 text-brand-primary font-medium'
: 'bg-surface-tertiary border-transparent text-text-secondary hover:bg-surface-tertiary/80'
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => {
setEditForm(f => ({
...f,
platforms: checked
? f.platforms.filter(p => p !== k)
: [...(f.platforms || []), k]
}))
}}
className="sr-only"
/>
{v.label}
</label>
)
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Notes</label>
<textarea
value={editForm.notes || ''}
onChange={e => setEditForm(f => ({ ...f, notes: 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="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button
onClick={() => setShowEditModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
>
Cancel
</button>
<button
onClick={saveCampaignEdit}
disabled={!editForm.name}
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"
>
Save Changes
</button>
</div>
</div>
</Modal>
{/* Post Detail Modal */}
<Modal
isOpen={!!selectedPost}

View File

@@ -6,11 +6,11 @@ import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { api, PLATFORMS } from '../utils/api'
import { PlatformIcons } from '../components/PlatformIcon'
import CampaignCalendar from '../components/CampaignCalendar'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
import BudgetBar from '../components/BudgetBar'
import InteractiveTimeline from '../components/InteractiveTimeline'
const EMPTY_CAMPAIGN = {
name: '', description: '', brand_id: '', status: 'planning',
@@ -241,8 +241,32 @@ export default function Campaigns() {
</div>
)}
{/* Calendar */}
<CampaignCalendar campaigns={filtered} />
{/* Timeline */}
<InteractiveTimeline
items={filtered}
mapItem={(campaign) => ({
id: campaign._id || campaign.id,
label: campaign.name,
description: campaign.description,
startDate: campaign.startDate || campaign.start_date || campaign.createdAt,
endDate: campaign.endDate || campaign.end_date,
status: campaign.status,
assigneeName: campaign.brandName || campaign.brand_name,
tags: campaign.platforms || [],
})}
onDateChange={async (campaignId, { startDate, endDate }) => {
try {
await api.patch(`/campaigns/${campaignId}`, { start_date: startDate, end_date: endDate })
} catch (err) {
console.error('Timeline date update failed:', err)
} finally {
loadCampaigns()
}
}}
onItemClick={(campaign) => {
navigate(`/campaigns/${campaign._id || campaign.id}`)
}}
/>
{/* Campaign list */}
<div className="bg-white rounded-xl border border-border overflow-hidden">
@@ -261,47 +285,50 @@ export default function Campaigns() {
return (
<div
key={campaign.id || campaign._id}
onClick={() => permissions?.canEditCampaigns ? navigate(`/campaigns/${campaign.id || campaign._id}`) : navigate(`/campaigns/${campaign.id || campaign._id}`)}
className="flex items-center gap-4 px-5 py-4 hover:bg-surface-secondary cursor-pointer transition-colors"
onClick={() => navigate(`/campaigns/${campaign.id || campaign._id}`)}
className="relative px-5 py-4 hover:bg-surface-secondary cursor-pointer transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-semibold text-text-primary">{campaign.name}</h4>
{campaign.brandName && <BrandBadge brand={campaign.brandName} />}
<ROIBadge revenue={campaign.revenue || 0} spent={spent} />
</div>
{campaign.description && (
<p className="text-xs text-text-secondary line-clamp-1">{campaign.description}</p>
)}
<div className="flex items-center gap-3 mt-1.5">
{campaign.platforms && campaign.platforms.length > 0 && (
<PlatformIcons platforms={campaign.platforms} size={16} />
)}
{budget > 0 && (
<div className="w-32">
<BudgetBar budget={budget} spent={spent} />
</div>
)}
</div>
{/* Quick metrics row */}
{(campaign.impressions > 0 || campaign.clicks > 0) && (
<div className="flex items-center gap-3 mt-1.5 text-[10px] text-text-tertiary">
{campaign.impressions > 0 && <span>👁 {campaign.impressions.toLocaleString()}</span>}
{campaign.clicks > 0 && <span>🖱 {campaign.clicks.toLocaleString()}</span>}
{campaign.conversions > 0 && <span>🎯 {campaign.conversions.toLocaleString()}</span>}
<div className="flex items-center gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-semibold text-text-primary">{campaign.name}</h4>
{campaign.brandName && <BrandBadge brand={campaign.brandName} />}
<ROIBadge revenue={campaign.revenue || 0} spent={spent} />
</div>
{campaign.description && (
<p className="text-xs text-text-secondary line-clamp-1">{campaign.description}</p>
)}
<div className="flex items-center gap-3 mt-1.5">
{budget > 0 && (
<div className="w-32">
<BudgetBar budget={budget} spent={spent} />
</div>
)}
{(campaign.impressions > 0 || campaign.clicks > 0) && (
<div className="flex items-center gap-3 text-[10px] text-text-tertiary">
{campaign.impressions > 0 && <span>👁 {campaign.impressions.toLocaleString()}</span>}
{campaign.clicks > 0 && <span>🖱 {campaign.clicks.toLocaleString()}</span>}
{campaign.conversions > 0 && <span>🎯 {campaign.conversions.toLocaleString()}</span>}
</div>
)}
</div>
</div>
<div className="text-right shrink-0">
<StatusBadge status={campaign.status} size="xs" />
<div className="text-xs text-text-tertiary mt-1">
{campaign.startDate && campaign.endDate ? (
<>
{format(new Date(campaign.startDate), 'MMM d')} {format(new Date(campaign.endDate), 'MMM d, yyyy')}
</>
) : '—'}
</div>
)}
</div>
<div className="text-right shrink-0">
<StatusBadge status={campaign.status} size="xs" />
<div className="text-xs text-text-tertiary mt-1">
{campaign.startDate && campaign.endDate ? (
<>
{format(new Date(campaign.startDate), 'MMM d')} {format(new Date(campaign.endDate), 'MMM d, yyyy')}
</>
) : '—'}
</div>
</div>
{campaign.platforms && campaign.platforms.length > 0 && (
<div className="flex justify-end mt-2">
<PlatformIcons platforms={campaign.platforms} size={16} />
</div>
)}
</div>
)
})

View File

@@ -8,6 +8,7 @@ import { api, PRIORITY_CONFIG } from '../utils/api'
import StatCard from '../components/StatCard'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import { SkeletonDashboard } from '../components/SkeletonLoader'
function getBudgetBarColor(percentage) {
if (percentage > 90) return 'bg-red-500'
@@ -180,16 +181,7 @@ export default function Dashboard() {
.slice(0, 8)
if (loading) {
return (
<div className="space-y-6 animate-pulse">
<div className="h-8 w-64 bg-surface-tertiary rounded-lg"></div>
<div className="grid grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-28 bg-surface-tertiary rounded-xl"></div>
))}
</div>
</div>
)
return <SkeletonDashboard />
}
return (

View File

@@ -56,7 +56,7 @@ export default function Login() {
onChange={(e) => setEmail(e.target.value)}
dir="auto"
className="w-full pl-11 pr-4 py-3 bg-slate-900/50 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
placeholder="f.mahidi@samayainvest.com"
placeholder="user@company.com"
required
autoFocus
/>
@@ -106,10 +106,10 @@ export default function Login() {
</button>
</form>
{/* Default Credentials */}
{/* Footer */}
<div className="mt-6 pt-6 border-t border-slate-700/50">
<p className="text-xs text-slate-500 text-center">
{t('login.defaultCreds')} <span className="text-slate-400 font-medium">f.mahidi@samayainvest.com</span> / <span className="text-slate-400 font-medium">admin123</span>
{t('login.forgotPassword')}
</p>
</div>
</div>

View File

@@ -9,6 +9,9 @@ import PostCard from '../components/PostCard'
import StatusBadge from '../components/StatusBadge'
import Modal from '../components/Modal'
import CommentsSection from '../components/CommentsSection'
import { SkeletonKanbanBoard, SkeletonTable } from '../components/SkeletonLoader'
import EmptyState from '../components/EmptyState'
import { useToast } from '../components/ToastContainer'
const EMPTY_POST = {
title: '', description: '', brand_id: '', platforms: [],
@@ -20,8 +23,10 @@ export default function PostProduction() {
const { t } = useLanguage()
const { teamMembers, brands } = useContext(AppContext)
const { canEditResource, canDeleteResource } = useAuth()
const toast = useToast()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [view, setView] = useState('kanban')
const [showModal, setShowModal] = useState(false)
const [editingPost, setEditingPost] = useState(null)
@@ -59,6 +64,7 @@ export default function PostProduction() {
const handleSave = async () => {
setPublishError('')
setSaving(true)
try {
const data = {
title: formData.title,
@@ -82,14 +88,17 @@ export default function PostProduction() {
if (missingPlatforms.length > 0) {
const names = missingPlatforms.map(p => PLATFORMS[p]?.label || p).join(', ')
setPublishError(`${t('posts.publishMissing')} ${names}`)
setSaving(false)
return
}
}
if (editingPost) {
await api.patch(`/posts/${editingPost._id}`, data)
toast.success(t('posts.updated'))
} else {
await api.post('/posts', data)
toast.success(t('posts.created'))
}
setShowModal(false)
setEditingPost(null)
@@ -100,19 +109,27 @@ export default function PostProduction() {
console.error('Save failed:', err)
if (err.message?.includes('Cannot publish')) {
setPublishError(err.message.replace(/.*: /, ''))
} else {
toast.error(t('common.saveFailed'))
}
} finally {
setSaving(false)
}
}
const handleMovePost = async (postId, newStatus) => {
try {
await api.patch(`/posts/${postId}`, { status: newStatus })
toast.success(t('posts.statusUpdated'))
loadPosts()
} catch (err) {
console.error('Move failed:', err)
if (err.message?.includes('Cannot publish')) {
setMoveError(t('posts.publishRequired'))
setTimeout(() => setMoveError(''), 5000)
toast.error(t('posts.publishRequired'))
} else {
toast.error(t('common.updateFailed'))
}
}
}
@@ -153,8 +170,10 @@ export default function PostProduction() {
try {
await api.delete(`/attachments/${attachmentId}`)
if (editingPost) loadAttachments(editingPost._id || editingPost.id)
toast.success(t('posts.attachmentDeleted'))
} catch (err) {
console.error('Delete attachment failed:', err)
toast.error(t('common.deleteFailed'))
}
}
@@ -244,14 +263,7 @@ export default function PostProduction() {
})
if (loading) {
return (
<div className="space-y-4 animate-pulse">
<div className="h-12 bg-surface-tertiary rounded-xl"></div>
<div className="flex gap-4">
{[...Array(5)].map((_, i) => <div key={i} className="w-72 h-96 bg-surface-tertiary rounded-xl"></div>)}
</div>
</div>
)
return view === 'kanban' ? <SkeletonKanbanBoard /> : <SkeletonTable rows={8} cols={6} />
}
return (
@@ -342,30 +354,38 @@ export default function PostProduction() {
<KanbanBoard posts={filteredPosts} onPostClick={openEdit} onMovePost={handleMovePost} />
) : (
<div className="bg-white rounded-xl border border-border overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-border bg-surface-secondary">
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.postTitle')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.brand')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.status')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.platforms')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.assignTo')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.scheduledDate')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-light">
{filteredPosts.map(post => (
<PostCard key={post._id} post={post} onClick={() => openEdit(post)} />
))}
{filteredPosts.length === 0 && (
<tr>
<td colSpan={6} className="py-12 text-center text-sm text-text-tertiary">
{t('posts.noPostsFound')}
</td>
{filteredPosts.length === 0 ? (
<EmptyState
icon={FileText}
title={posts.length === 0 ? t('posts.noPosts') : t('posts.noPostsFound')}
description={posts.length === 0 ? t('posts.createFirstPost') : t('posts.tryDifferentFilter')}
actionLabel={posts.length === 0 ? t('posts.createPost') : null}
onAction={posts.length === 0 ? openNew : null}
secondaryActionLabel={posts.length > 0 ? t('common.clearFilters') : null}
onSecondaryAction={() => {
setFilters({ brand: '', platform: '', assignedTo: '', campaign: '' })
setSearchTerm('')
}}
/>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-surface-secondary">
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.postTitle')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.brand')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.status')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.platforms')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.assignTo')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.scheduledDate')}</th>
</tr>
)}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-border-light">
{filteredPosts.map(post => (
<PostCard key={post._id} post={post} onClick={() => openEdit(post)} />
))}
</tbody>
</table>
)}
</div>
)}
@@ -738,8 +758,8 @@ export default function PostProduction() {
</button>
<button
onClick={handleSave}
disabled={!formData.title}
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"
disabled={!formData.title || saving}
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 ${saving ? 'btn-loading' : ''}`}
>
{editingPost ? t('posts.saveChanges') : t('posts.createPost')}
</button>
@@ -759,11 +779,13 @@ export default function PostProduction() {
if (editingPost) {
try {
await api.delete(`/posts/${editingPost._id || editingPost.id}`)
toast.success(t('posts.deleted'))
setShowModal(false)
setEditingPost(null)
loadPosts()
} catch (err) {
console.error('Delete failed:', err)
toast.error(t('common.deleteFailed'))
}
}
}}

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useContext } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
ArrowLeft, Plus, Check, Trash2, Edit3, LayoutGrid, List,
GanttChart, Settings, Calendar, Clock
GanttChart, Settings, Calendar, Clock, MessageCircle, X
} from 'lucide-react'
import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore } from 'date-fns'
import { AppContext } from '../App'
@@ -24,7 +24,6 @@ export default function ProjectDetail() {
const navigate = useNavigate()
const { teamMembers, brands } = useContext(AppContext)
const { permissions, canEditResource, canDeleteResource } = useAuth()
const canEditProject = canEditResource('project', project)
const canManageProject = permissions?.canEditProjects
const [project, setProject] = useState(null)
const [tasks, setTasks] = useState([])
@@ -37,12 +36,14 @@ export default function ProjectDetail() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [taskToDelete, setTaskToDelete] = useState(null)
const [taskForm, setTaskForm] = useState({
title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo'
title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo'
})
const [projectForm, setProjectForm] = useState({
name: '', description: '', brand_id: '', owner_id: '', status: 'active', due_date: ''
name: '', description: '', brand_id: '', owner_id: '', status: 'active', start_date: '', due_date: ''
})
const [showDiscussion, setShowDiscussion] = useState(false)
// Drag state for kanban
const [draggedTask, setDraggedTask] = useState(null)
const [dragOverCol, setDragOverCol] = useState(null)
@@ -72,6 +73,7 @@ export default function ProjectDetail() {
description: taskForm.description,
priority: taskForm.priority,
assigned_to: taskForm.assigned_to ? Number(taskForm.assigned_to) : null,
start_date: taskForm.start_date || null,
due_date: taskForm.due_date || null,
status: taskForm.status,
project_id: Number(id),
@@ -83,7 +85,7 @@ export default function ProjectDetail() {
}
setShowTaskModal(false)
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo' })
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
loadProject()
} catch (err) {
console.error('Task save failed:', err)
@@ -122,6 +124,7 @@ export default function ProjectDetail() {
description: task.description || '',
priority: task.priority || 'medium',
assigned_to: task.assignedTo || task.assigned_to || '',
start_date: task.startDate || task.start_date ? new Date(task.startDate || task.start_date).toISOString().slice(0, 10) : '',
due_date: task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 10) : '',
status: task.status || 'todo',
})
@@ -130,7 +133,7 @@ export default function ProjectDetail() {
const openNewTask = () => {
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo' })
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
setShowTaskModal(true)
}
@@ -142,6 +145,7 @@ export default function ProjectDetail() {
brand_id: project.brandId || project.brand_id || '',
owner_id: project.ownerId || project.owner_id || '',
status: project.status || 'active',
start_date: project.startDate || project.start_date ? new Date(project.startDate || project.start_date).toISOString().slice(0, 10) : '',
due_date: project.dueDate ? new Date(project.dueDate).toISOString().slice(0, 10) : '',
})
setShowProjectModal(true)
@@ -155,6 +159,7 @@ export default function ProjectDetail() {
brand_id: projectForm.brand_id ? Number(projectForm.brand_id) : null,
owner_id: projectForm.owner_id ? Number(projectForm.owner_id) : null,
status: projectForm.status,
start_date: projectForm.start_date || null,
due_date: projectForm.due_date || null,
})
setShowProjectModal(false)
@@ -212,13 +217,16 @@ export default function ProjectDetail() {
)
}
const canEditProject = canEditResource('project', project)
const completedTasks = tasks.filter(t => t.status === 'done').length
const progress = tasks.length > 0 ? Math.round((completedTasks / tasks.length) * 100) : 0
const ownerName = project.ownerName || project.owner_name
const brandName = project.brandName || project.brand_name
return (
<div className="space-y-6 animate-fade-in">
<div className="flex gap-6 animate-fade-in">
{/* Main content */}
<div className={`space-y-6 min-w-0 ${showDiscussion ? 'flex-1' : 'w-full'}`}>
{/* Back button */}
<button
onClick={() => navigate('/projects')}
@@ -251,15 +259,26 @@ export default function ProjectDetail() {
)}
</div>
</div>
{canEditProject && (
<div className="flex items-center gap-2">
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
onClick={() => setShowDiscussion(prev => !prev)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
showDiscussion ? 'bg-brand-primary text-white' : 'text-text-secondary hover:text-text-primary hover:bg-surface-tertiary'
}`}
>
<Settings className="w-4 h-4" />
Edit
<MessageCircle className="w-4 h-4" />
Discussion
</button>
)}
{canEditProject && (
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
>
<Settings className="w-4 h-4" />
Edit
</button>
)}
</div>
</div>
{project.description && (
@@ -282,11 +301,6 @@ export default function ProjectDetail() {
</div>
</div>
{/* Discussion */}
<div className="bg-white rounded-xl border border-border p-6">
<CommentsSection entityType="project" entityId={Number(id)} />
</div>
{/* View switcher + Add Task */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 bg-surface-tertiary rounded-lg p-0.5">
@@ -432,6 +446,25 @@ export default function ProjectDetail() {
{/* ─── GANTT / TIMELINE VIEW ─── */}
{view === 'gantt' && <GanttView tasks={tasks} project={project} onEditTask={openEditTask} />}
</div>{/* end main content */}
{/* ─── DISCUSSION SIDEBAR ─── */}
{showDiscussion && (
<div className="w-[340px] shrink-0 bg-white rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-1.5">
<MessageCircle className="w-4 h-4" />
Discussion
</h3>
<button onClick={() => setShowDiscussion(false)} className="p-1 hover:bg-surface-tertiary rounded-lg text-text-tertiary">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<CommentsSection entityType="project" entityId={Number(id)} />
</div>
</div>
)}
{/* ─── TASK MODAL ─── */}
<Modal
@@ -482,14 +515,19 @@ export default function ProjectDetail() {
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Assign To</label>
<select value={taskForm.assigned_to} onChange={e => setTaskForm(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="">Unassigned</option>
{assignableUsers.map(m => <option key={m._id || m.id} value={m._id || m.id}>{m.name}</option>)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Assign To</label>
<select value={taskForm.assigned_to} onChange={e => setTaskForm(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="">Unassigned</option>
{assignableUsers.map(m => <option key={m._id || m.team_member_id} value={m._id || m.team_member_id}>{m.name}</option>)}
</select>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={taskForm.start_date} onChange={e => setTaskForm(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-sm font-medium text-text-primary mb-1">Due Date</label>
@@ -566,11 +604,16 @@ export default function ProjectDetail() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={projectForm.due_date} onChange={e => setProjectForm(f => ({ ...f, due_date: e.target.value }))}
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={projectForm.start_date} onChange={e => setProjectForm(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>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={projectForm.due_date} onChange={e => setProjectForm(f => ({ ...f, due_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 className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button onClick={() => setShowProjectModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg">
@@ -698,7 +741,9 @@ function GanttView({ tasks, project, onEditTask }) {
const dayWidth = Math.max(36, Math.min(60, 800 / totalDays))
const getBarStyle = (task) => {
const start = task.createdAt ? startOfDay(new Date(task.createdAt)) : today
const start = task.startDate || task.start_date
? startOfDay(new Date(task.startDate || task.start_date))
: task.createdAt ? startOfDay(new Date(task.createdAt)) : today
const end = task.dueDate ? startOfDay(new Date(task.dueDate)) : addDays(start, 3)
const left = differenceInDays(start, earliest) * dayWidth
const width = Math.max(dayWidth, (differenceInDays(end, start) + 1) * dayWidth)
@@ -775,7 +820,7 @@ function GanttView({ tasks, project, onEditTask }) {
})}
</div>
</div>
</div>
)
}

View File

@@ -1,17 +1,20 @@
import { useState, useEffect, useContext } from 'react'
import { Plus, Search, FolderKanban } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { Plus, Search, FolderKanban, LayoutGrid, GanttChart } from 'lucide-react'
import { AppContext } from '../App'
import { api } from '../utils/api'
import { useAuth } from '../contexts/AuthContext'
import ProjectCard from '../components/ProjectCard'
import Modal from '../components/Modal'
import InteractiveTimeline from '../components/InteractiveTimeline'
const EMPTY_PROJECT = {
name: '', description: '', brand_id: '', status: 'active',
owner_id: '', due_date: '',
owner_id: '', start_date: '', due_date: '',
}
export default function Projects() {
const navigate = useNavigate()
const { teamMembers, brands } = useContext(AppContext)
const { permissions } = useAuth()
const [projects, setProjects] = useState([])
@@ -19,6 +22,7 @@ export default function Projects() {
const [showModal, setShowModal] = useState(false)
const [formData, setFormData] = useState(EMPTY_PROJECT)
const [searchTerm, setSearchTerm] = useState('')
const [view, setView] = useState('timeline') // 'grid' | 'timeline'
useEffect(() => { loadProjects() }, [])
@@ -41,6 +45,7 @@ export default function Projects() {
brand_id: formData.brand_id ? Number(formData.brand_id) : null,
owner_id: formData.owner_id ? Number(formData.owner_id) : null,
status: formData.status,
start_date: formData.start_date || null,
due_date: formData.due_date || null,
}
await api.post('/projects', data)
@@ -83,6 +88,25 @@ export default function Projects() {
/>
</div>
{/* View switcher */}
<div className="flex items-center gap-1 bg-surface-tertiary rounded-lg p-0.5">
{[
{ id: 'grid', icon: LayoutGrid, label: 'Grid' },
{ id: 'timeline', icon: GanttChart, label: 'Timeline' },
].map(v => (
<button
key={v.id}
onClick={() => setView(v.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
view === v.id ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary hover:text-text-secondary'
}`}
>
<v.icon className="w-4 h-4" />
{v.label}
</button>
))}
</div>
{permissions?.canCreateProjects && (
<button
onClick={() => setShowModal(true)}
@@ -94,19 +118,46 @@ export default function Projects() {
)}
</div>
{/* Project grid */}
{/* Content */}
{filtered.length === 0 ? (
<div className="py-20 text-center">
<FolderKanban className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
<p className="text-text-secondary font-medium">No projects yet</p>
<p className="text-sm text-text-tertiary mt-1">Create your first project to start organizing work</p>
</div>
) : (
) : view === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 stagger-children">
{filtered.map(project => (
<ProjectCard key={project._id} project={project} />
))}
</div>
) : (
<InteractiveTimeline
items={filtered}
mapItem={(project) => ({
id: project._id || project.id,
label: project.name,
description: project.description,
startDate: project.startDate || project.start_date || project.createdAt,
endDate: project.dueDate || project.due_date,
status: project.status,
priority: project.priority,
assigneeName: project.ownerName || project.owner_name,
tags: [project.status, project.priority].filter(Boolean),
})}
onDateChange={async (projectId, { startDate, endDate }) => {
try {
await api.patch(`/projects/${projectId}`, { start_date: startDate, due_date: endDate })
} catch (err) {
console.error('Timeline date update failed:', err)
} finally {
loadProjects()
}
}}
onItemClick={(project) => {
navigate(`/projects/${project._id || project.id}`)
}}
/>
)}
{/* Create Modal */}
@@ -174,16 +225,26 @@ export default function Projects() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(f => ({ ...f, due_date: e.target.value }))}
value={formData.start_date}
onChange={e => setFormData(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>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(f => ({ ...f, due_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 className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button
onClick={() => setShowModal(false)}

View File

@@ -7,13 +7,17 @@ import { api } from '../utils/api'
import TaskCard from '../components/TaskCard'
import Modal from '../components/Modal'
import CommentsSection from '../components/CommentsSection'
import EmptyState from '../components/EmptyState'
import { useToast } from '../components/ToastContainer'
export default function Tasks() {
const { t } = useLanguage()
const { currentUser } = useContext(AppContext)
const { user: authUser, canEditResource, canDeleteResource } = useAuth()
const toast = useToast()
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [showModal, setShowModal] = useState(false)
const [editingTask, setEditingTask] = useState(null)
const [draggedTask, setDraggedTask] = useState(null)
@@ -24,7 +28,7 @@ export default function Tasks() {
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: ''
title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: ''
})
const isSuperadmin = authUser?.role === 'superadmin'
@@ -55,29 +59,30 @@ export default function Tasks() {
if (filterView === 'all') return true
if (filterView === 'assigned_to_me') {
// Tasks where I'm the assignee (via team_member_id on my user record)
const myTeamMemberId = authUser?.team_member_id
return myTeamMemberId && task.assigned_to === myTeamMemberId
return task.assignedTo === authUser?.id || task.assigned_to === authUser?.id
}
if (filterView === 'created_by_me') {
return task.created_by_user_id === authUser?.id
return task.createdByUserId === authUser?.id || task.created_by_user_id === authUser?.id
}
// Superadmin filtering by specific team member (assigned_to = member id)
// Superadmin filtering by specific team member
if (isSuperadmin && !isNaN(Number(filterView))) {
return task.assigned_to === Number(filterView)
const memberId = Number(filterView)
return task.assignedTo === memberId || task.assigned_to === memberId
}
return true
})
const handleSave = async () => {
setSaving(true)
try {
const data = {
title: formData.title,
description: formData.description,
priority: formData.priority,
start_date: formData.start_date || null,
due_date: formData.due_date || null,
status: formData.status,
assigned_to: formData.assigned_to || null,
@@ -85,29 +90,38 @@ export default function Tasks() {
}
if (editingTask) {
await api.patch(`/tasks/${editingTask._id || editingTask.id}`, data)
toast.success(t('tasks.updated'))
} else {
await api.post('/tasks', data)
toast.success(t('tasks.created'))
}
setShowModal(false)
setEditingTask(null)
setFormData({ title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: '' })
setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' })
loadTasks()
} catch (err) {
console.error('Save failed:', err)
if (err.message?.includes('403') || err.message?.includes('modify your own')) {
alert('You can only edit your own tasks')
toast.error(t('tasks.canOnlyEditOwn'))
} else {
toast.error(t('common.saveFailed'))
}
} finally {
setSaving(false)
}
}
const handleMove = async (taskId, newStatus) => {
try {
await api.patch(`/tasks/${taskId}`, { status: newStatus })
toast.success(t('tasks.statusUpdated'))
loadTasks()
} catch (err) {
console.error('Move failed:', err)
if (err.message?.includes('403')) {
alert('You can only modify your own tasks')
toast.error(t('tasks.canOnlyEditOwn'))
} else {
toast.error(t('common.updateFailed'))
}
}
}
@@ -119,6 +133,7 @@ export default function Tasks() {
title: task.title || '',
description: task.description || '',
priority: task.priority || 'medium',
start_date: task.start_date || task.startDate || '',
due_date: task.due_date || task.dueDate || '',
status: task.status || 'todo',
assigned_to: task.assigned_to || '',
@@ -136,10 +151,12 @@ export default function Tasks() {
if (!taskToDelete) return
try {
await api.delete(`/tasks/${taskToDelete._id || taskToDelete.id}`)
toast.success(t('tasks.deleted'))
setTaskToDelete(null)
loadTasks()
} catch (err) {
console.error('Delete failed:', err)
toast.error(t('common.deleteFailed'))
}
}
@@ -232,7 +249,7 @@ export default function Tasks() {
</p>
</div>
<button
onClick={() => { setEditingTask(null); setFormData({ title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: '' }); setShowModal(true) }}
onClick={() => { setEditingTask(null); setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' }); setShowModal(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"
>
<Plus className="w-4 h-4" />
@@ -242,15 +259,19 @@ export default function Tasks() {
{/* Task columns */}
{filteredTasks.length === 0 ? (
<div className="py-20 text-center">
<CheckSquare className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
<p className="text-text-secondary font-medium">
{tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatch')}
</p>
<p className="text-sm text-text-tertiary mt-1">
{tasks.length === 0 ? t('tasks.createFirst') : t('tasks.tryFilter')}
</p>
</div>
<EmptyState
icon={CheckSquare}
title={tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatch')}
description={tasks.length === 0 ? t('tasks.createFirst') : t('tasks.tryFilter')}
actionLabel={tasks.length === 0 ? t('tasks.createTask') : null}
onAction={tasks.length === 0 ? () => {
setEditingTask(null)
setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' })
setShowModal(true)
} : null}
secondaryActionLabel={tasks.length > 0 ? t('common.clearFilters') : null}
onSecondaryAction={() => setFilterView('all')}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{columns.map(col => {
@@ -291,29 +312,18 @@ export default function Tasks() {
onDragEnd={handleDragEnd}
className={canEdit ? 'cursor-grab active:cursor-grabbing' : ''}
>
<div className="relative group">
<div className="relative group" onClick={() => canEdit && openEdit(task)}>
<TaskCard task={task} onMove={canEdit ? handleMove : undefined} showProject />
{/* Edit/Delete overlay */}
{(canEdit || canDelete) && (
{/* Delete overlay */}
{canDelete && (
<div className="absolute top-2 right-2 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
{canEdit && (
<button
onClick={(e) => { e.stopPropagation(); openEdit(task) }}
className="p-1 hover:bg-surface-tertiary rounded text-text-tertiary hover:text-text-primary"
title={t('tasks.editTask')}
>
<Edit2 className="w-3.5 h-3.5" />
</button>
)}
{canDelete && (
<button
onClick={(e) => { e.stopPropagation(); handleDelete(task) }}
className="p-1 hover:bg-red-50 rounded text-text-tertiary hover:text-red-500"
title={t('common.delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
<button
onClick={(e) => { e.stopPropagation(); handleDelete(task) }}
className="p-1 hover:bg-red-50 rounded text-text-tertiary hover:text-red-500"
title={t('common.delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
@@ -382,16 +392,26 @@ export default function Tasks() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('tasks.dueDate')}</label>
<label className="block text-sm font-medium text-text-primary mb-1">{t('timeline.startDate')}</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(f => ({ ...f, due_date: e.target.value }))}
value={formData.start_date}
onChange={e => setFormData(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>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('tasks.dueDate')}</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(f => ({ ...f, due_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>
{/* Comments (only for existing tasks) */}
{editingTask && (
<CommentsSection entityType="task" entityId={editingTask._id || editingTask.id} />
@@ -406,8 +426,8 @@ export default function Tasks() {
</button>
<button
onClick={handleSave}
disabled={!formData.title}
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"
disabled={!formData.title || saving}
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 ${saving ? 'btn-loading' : ''}`}
>
{editingTask ? t('tasks.saveChanges') : t('tasks.createTask')}
</button>

View File

@@ -388,7 +388,7 @@ export default function Team() {
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="Samaya Investment, Hira Cultural District"
placeholder="Brand A, Brand B"
/>
<p className="text-xs text-text-tertiary mt-1">{t('team.brandsHelp')}</p>
</div>

View File

@@ -240,7 +240,7 @@ export default function Users() {
value={form.email}
onChange={e => setForm(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="user@samayainvest.com"
placeholder="user@company.com"
required
/>
</div>