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 (
{t('postDetail.noAssets')}
) : piecesReady ? ({piece.content_preview}
)} {isCopy && piece.languages && piece.languages.length > 0 && ({piece.language}
)} {/* Design/Video: version info */} {!isCopy && piece.current_version && (v{piece.current_version}
)} {/* Approval info */}
{t('postDetail.notLinked')}
{t('common.noResults')}
) : ( filteredCandidates.slice(0, 10).map(c => ( )) )}