import { useState, useEffect, useContext, useCallback, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { ArrowLeft, Save, FileText, Image as ImageIcon, Film, Type, Search, Link2, Unlink, Plus, CheckCircle, Clock, X, ExternalLink } from 'lucide-react' import { AppContext } from '../App' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../i18n/LanguageContext' import { api, PLATFORMS } from '../utils/api' import PlatformIcon from '../components/PlatformIcon' import StatusBadge from '../components/StatusBadge' import PortalSelect from '../components/PortalSelect' import CommentsSection from '../components/CommentsSection' import ArtefactDetailPanel from '../components/ArtefactDetailPanel' import { useToast } from '../components/ToastContainer' const STATUS_OPTS = ['draft', 'in_review', 'approved', 'rejected', 'scheduled', 'published'] // Maps asset type key → composition field name const PIECE_MAP = { caption: 'caption', body: 'body_copy', design: 'design', video: 'video' } // Maps asset type key → i18n label key const LABEL_KEYS = { caption: 'postDetail.captionCopy', body: 'postDetail.bodyCopy', design: 'postDetail.design', video: 'postDetail.video', } const ASSET_ICONS = { caption: Type, body: FileText, design: ImageIcon, video: Film } const ASSET_TYPES = ['caption', 'body', 'design', 'video'] // Maps server-generated waiting_on labels → asset type key const WAITING_TYPE_MAP = { Caption: 'caption', Copy: 'body', Design: 'design', Video: 'video' } export default function PostDetail() { const { id } = useParams() const navigate = useNavigate() const { brands, getBrandName, teamMembers } = useContext(AppContext) const { t, lang } = useLanguage() const { user } = useAuth() const toast = useToast() const [post, setPost] = useState(null) const [composition, setComposition] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [campaigns, setCampaigns] = useState([]) // Editable form fields const [title, setTitle] = useState('') const [status, setStatus] = useState('draft') const [brandId, setBrandId] = useState('') const [campaignId, setCampaignId] = useState('') const [assignedTo, setAssignedTo] = useState('') const [platforms, setPlatforms] = useState([]) const [scheduledDate, setScheduledDate] = useState('') // Link pickers / create const [creating, setCreating] = useState(false) const [activePicker, setActivePicker] = useState(null) // 'caption' | 'body' | 'design' | 'video' const [pickerSearch, setPickerSearch] = useState('') const [linkCandidates, setLinkCandidates] = useState([]) const [linking, setLinking] = useState(false) const allArtefactsRef = useRef(null) // Sub-panels const [openArtefact, setOpenArtefact] = useState(null) const loadPost = useCallback(async () => { try { const [p, comp] = await Promise.all([ api.get(`/posts/${id}`), api.get(`/posts/${id}/composition`), ]) setPost(p) setComposition(comp) setTitle(p.title || '') setStatus(p.status || 'draft') setBrandId(p.brand_id || p.brandId || '') setCampaignId(p.campaign_id || p.campaignId || '') setAssignedTo(p.assigned_to || p.assignedTo || '') const plats = p.platforms || (p.platform ? [p.platform] : []) setPlatforms(Array.isArray(plats) ? plats : []) const sd = p.scheduled_date || p.scheduledDate setScheduledDate(sd ? new Date(sd).toISOString().slice(0, 10) : '') } catch (err) { console.error('Failed to load post:', err) } finally { setLoading(false) } }, [id]) useEffect(() => { loadPost() api.get('/campaigns').then(r => setCampaigns(Array.isArray(r) ? r : [])).catch(() => {}) }, [loadPost]) const loadComposition = useCallback(async () => { try { setComposition(await api.get(`/posts/${id}/composition`)) } catch (err) { console.error('Failed to load composition:', err) } }, [id]) const handleSave = async () => { setSaving(true) try { await api.patch(`/posts/${id}`, { title, status, brand_id: brandId ? Number(brandId) : null, campaign_id: campaignId ? Number(campaignId) : null, assigned_to: assignedTo ? Number(assignedTo) : null, platforms, scheduled_date: scheduledDate || null, }) toast.success(t('posts.updated')) // Update local post state — composition is unaffected by metadata changes setPost(p => ({ ...p, title, status, brand_id: brandId, campaign_id: campaignId, assigned_to: assignedTo, platforms, scheduled_date: scheduledDate || null })) } catch { toast.error(t('common.saveFailed')) } finally { setSaving(false) } } const togglePlatform = (key) => { setPlatforms(prev => prev.includes(key) ? prev.filter(p => p !== key) : [...prev, key]) } // ─── Link / Unlink / Create ─── const TYPE_FILTERS = { caption: a => a.type === 'copy' && a.copy_type === 'caption', body: a => a.type === 'copy' && (a.copy_type === 'body' || !a.copy_type), video: a => a.type === 'video', design: a => (a.type || 'design') === 'design', } const openLinkPicker = async (type) => { setActivePicker(type) setPickerSearch('') try { if (!allArtefactsRef.current) allArtefactsRef.current = await api.get('/artefacts') const all = Array.isArray(allArtefactsRef.current) ? allArtefactsRef.current : [] setLinkCandidates(all.filter(a => { const linkedTo = a.post_id || a.postId return TYPE_FILTERS[type](a) && (!linkedTo || String(linkedTo) !== String(id)) })) } catch { setLinkCandidates([]) toast.error(t('common.error')) } } const handleLink = async (itemId) => { setLinking(true) try { await api.patch(`/artefacts/${itemId}`, { post_id: Number(id) }) allArtefactsRef.current = null toast.success(t('posts.updated')) setActivePicker(null) loadComposition() } catch { toast.error(t('common.saveFailed')) } finally { setLinking(false) } } const handleUnlink = async (type) => { const piece = composition?.[PIECE_MAP[type]] if (!piece) return try { await api.patch(`/artefacts/${piece.id}`, { post_id: null }) allArtefactsRef.current = null toast.success(t('posts.updated')) loadComposition() } catch { toast.error(t('common.saveFailed')) } } const handleOpenPiece = async (type) => { const piece = composition?.[PIECE_MAP[type]] if (!piece) return try { const full = await api.get(`/artefacts/${piece.id}`) setOpenArtefact(full) } catch { toast.error(t('common.saveFailed')) } } const handleCreate = async (type) => { if (creating) return setCreating(true) try { const created = await api.post('/artefacts', { title: title.trim() ? `${t(LABEL_KEYS[type])} — ${title.trim()}` : t(LABEL_KEYS[type]), type: type === 'caption' || type === 'body' ? 'copy' : type, copy_type: type === 'caption' ? 'caption' : type === 'body' ? 'body' : undefined, post_id: Number(id), }) allArtefactsRef.current = null setOpenArtefact(created) loadComposition() } catch { toast.error(t('common.saveFailed')) } finally { setCreating(false) } } // ─── Rendering ─── if (loading) { return (
{[1,2,3,4].map(i =>
)}
) } if (!post) { return (
{t('common.noResults')}{' '}
) } const filteredCandidates = linkCandidates.filter(c => { if (!pickerSearch) return true return (c.title || '').toLowerCase().includes(pickerSearch.toLowerCase()) }) const isDirty = Boolean(post) && ( title !== (post.title || '') || status !== (post.status || 'draft') || String(brandId) !== String(post.brand_id || post.brandId || '') || String(campaignId) !== String(post.campaign_id || post.campaignId || '') || String(assignedTo) !== String(post.assigned_to || post.assignedTo || '') || JSON.stringify(platforms) !== JSON.stringify(Array.isArray(post.platforms) ? post.platforms : (post.platform ? [post.platform] : [])) || scheduledDate !== ((post.scheduled_date || post.scheduledDate) ? new Date(post.scheduled_date || post.scheduledDate).toISOString().slice(0, 10) : '') ) const waitingOn = composition?.waiting_on || [] const piecesReady = composition?.pieces_ready || false const hasPieces = composition?.caption || composition?.body_copy || composition?.design || composition?.video return (
{/* ─── HEADER ─── */}
setTitle(e.target.value)} className="flex-1 text-xl font-bold text-text-primary bg-transparent border-none outline-none focus:ring-0 placeholder:text-text-tertiary" placeholder={t('posts.postTitlePlaceholder')} />
setStatus(val)} options={STATUS_OPTS.map(s => ({ value: s, label: t(`posts.status.${s}`) }))} className="text-xs" /> setBrandId(val)} options={[ { value: '', label: t('posts.selectBrand') }, ...brands.map(b => ({ value: String(b._id), label: lang === 'ar' && b.name_ar ? b.name_ar : b.name })) ]} placeholder={t('posts.selectBrand')} className="text-xs" /> setCampaignId(val)} options={[ { value: '', label: t('posts.noCampaign') }, ...campaigns.map(c => ({ value: String(c._id || c.id), label: c.name })) ]} placeholder={t('posts.noCampaign')} className="text-xs" /> setAssignedTo(val)} options={[ { value: '', label: t('common.unassigned') }, ...teamMembers.map(m => ({ value: String(m._id), label: m.name })) ]} placeholder={t('common.unassigned')} className="text-xs" />
{/* Platforms */}
{Object.entries(PLATFORMS).map(([key, p]) => ( ))}
{/* Date + Save */}
setScheduledDate(e.target.value)} className="text-xs border border-border rounded-lg px-2.5 py-1.5 bg-surface text-text-secondary focus:outline-none focus:ring-2 focus:ring-brand-primary/20" />
{/* ─── ASSET CARDS ─── */}
{ASSET_TYPES.map(type => ( handleCreate(type)} creating={creating} onOpen={() => handleOpenPiece(type)} onUnlink={() => handleUnlink(type)} onOpenPicker={() => openLinkPicker(type)} activePicker={activePicker} pickerSearch={pickerSearch} filteredCandidates={filteredCandidates} linking={linking} onLink={handleLink} onPickerSearchChange={setPickerSearch} onClosePicker={() => setActivePicker(null)} t={t} /> ))}
{/* ─── READINESS ─── */}

{t('postDetail.readiness')}

{!hasPieces ? (

{t('postDetail.noAssets')}

) : piecesReady ? (
{t('postDetail.allPiecesApproved')}
) : (
{t('postDetail.waitingOn')}: {waitingOn.map(label => { const type = WAITING_TYPE_MAP[label] return type ? ( ) : {label} })}
)}
{/* ─── COMMENTS ─── */}

{t('posts.discussion')}

{/* ─── SUB-PANELS (they render their own SlidePanel internally) ─── */} {openArtefact && ( { setOpenArtefact(null); loadComposition() }} onUpdate={loadComposition} onDelete={() => { setOpenArtefact(null); loadComposition() }} assignableUsers={teamMembers} /> )}
) } // ─── Asset Card Component ─── function AssetCard({ id, type, label, icon: Icon, piece, onCreate, creating, onOpen, onUnlink, onOpenPicker, activePicker, pickerSearch, filteredCandidates, linking, onLink, onPickerSearchChange, onClosePicker, t, }) { const isPickerOpen = activePicker === type const isCopy = type === 'caption' || type === 'body' const isPending = piece?.status === 'pending_review' const isApproved = piece?.status === 'approved' return (
{/* Header */}

{label}

{/* ─── State 2: Linked ─── */} {piece && ( <>
{/* Thumbnail for design/video */} {!isCopy && piece.thumbnail_url && (
{piece.title}
)} {!isCopy && !piece.thumbnail_url && (
)}
{piece.title}
{/* Copy: content preview + languages */} {isCopy && piece.content_preview && (

{piece.content_preview}

)} {isCopy && piece.languages && piece.languages.length > 0 && (
{piece.languages.map((l, i) => ( {l.language} ))}
)} {isCopy && (!piece.languages || piece.languages.length === 0) && piece.language && (

{piece.language}

)} {/* Design/Video: version info */} {!isCopy && piece.current_version && (

v{piece.current_version}

)} {/* Approval info */}
{isPending && piece.approver_name && (

{t('postDetail.pendingReviewBy')} {piece.approver_name}

)} {isApproved && (

{t('postDetail.approved')}{piece.approver_name ? ` — ${piece.approver_name}` : ''}

)}
{/* Open + Unlink */}
)} {/* ─── State 1: Empty (no asset) ─── */} {!piece && ( <>

{t('postDetail.notLinked')}

{!isPickerOpen && (
)} )} {/* Inline link picker */} {isPickerOpen && (
onPickerSearchChange(e.target.value)} placeholder={t('common.search')} className="w-full ps-7 pe-2 py-1.5 text-xs border border-border rounded-lg bg-surface focus:outline-none focus:ring-2 focus:ring-brand-primary/20" autoFocus />
{filteredCandidates.length === 0 ? (

{t('common.noResults')}

) : ( filteredCandidates.slice(0, 10).map(c => ( )) )}
)}
) }