diff --git a/client/src/components/ArtefactDetailPanel.jsx b/client/src/components/ArtefactDetailPanel.jsx index 9c3b02c..af465cc 100644 --- a/client/src/components/ArtefactDetailPanel.jsx +++ b/client/src/components/ArtefactDetailPanel.jsx @@ -24,6 +24,10 @@ const TYPE_ICONS = { other: Sparkles, } +const parseApproverIds = (a) => + a.approvers?.map(u => String(u.id)) || + (a.approver_ids ? a.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []) + export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDelete, assignableUsers = [] }) { const { t } = useLanguage() const { brands } = useContext(AppContext) @@ -35,14 +39,12 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const [submitting, setSubmitting] = useState(false) const [freshReviewUrl, setFreshReviewUrl] = useState('') const [copied, setCopied] = useState(false) - const [activeTab, setActiveTab] = useState('details') + const [activeTab, setActiveTab] = useState(artefact.type === 'copy' ? 'versions' : 'details') - // Editable fields + // Editable fields — seeded from artefact prop; component is keyed by artefact._id at call site const [editTitle, setEditTitle] = useState(artefact.title || '') const [editDescription, setEditDescription] = useState(artefact.description || '') - const [editApproverIds, setEditApproverIds] = useState( - artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []) - ) + const [editApproverIds, setEditApproverIds] = useState(() => parseApproverIds(artefact)) const reviewUrl = freshReviewUrl || (artefact.approval_token ? `${window.location.origin}/review/${artefact.approval_token}` : '') const [savingDraft, setSavingDraft] = useState(false) const [deleting, setDeleting] = useState(false) @@ -61,14 +63,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel loadVersions() }, [artefact.Id]) - useEffect(() => { - setEditTitle(artefact.title || '') - setEditDescription(artefact.description || '') - setEditApproverIds( - artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []) - ) - }, [artefact.Id]) - const loadVersions = async () => { try { const res = await api.get(`/artefacts/${artefact.Id}/versions`) @@ -109,13 +103,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel loadVersionData(version.Id) } - const handleCreateVersion = async ({ notes, copy_from_previous }) => { - await api.post(`/artefacts/${artefact.Id}/versions`, { notes, copy_from_previous }) - toast.success(t('artefacts.versionCreated')) - loadVersions() - onUpdate() - } - const handleAddLanguage = async (languageForm) => { await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/texts`, languageForm) toast.success(t('artefacts.languageAdded')) @@ -249,6 +236,12 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel } } + const handleUpdateLanguage = async (textId, content) => { + await api.patch(`/artefact-version-texts/${textId}`, { content }) + toast.success(t('artefacts.languageAdded')) + loadVersionData(selectedVersion.Id) + } + const handleDeleteArtefact = async () => { setDeleting(true) try { @@ -282,10 +275,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const TypeIcon = TYPE_ICONS[artefact.type] || Sparkles const tabs = [ - { key: 'details', label: t('artefacts.details') || 'Details', icon: FileEdit }, - { key: 'versions', label: t('artefacts.versions') || 'Versions', icon: Layers, badge: versions.length }, - { key: 'discussion', label: t('artefacts.comments') || 'Discussion', icon: MessageSquare, badge: comments.length }, - { key: 'review', label: t('artefacts.review') || 'Review', icon: ShieldCheck }, + { key: 'details', label: t('artefacts.details'), icon: FileEdit }, + { key: 'versions', label: t('artefacts.versions'), icon: Layers, badge: versions.length }, + { key: 'discussion', label: t('artefacts.comments'), icon: MessageSquare, badge: comments.length }, + { key: 'review', label: t('artefacts.review'), icon: ShieldCheck }, ] if (loading) { @@ -304,32 +297,30 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel onClose={onClose} size="xl" header={ - <> -
+ {brands.find(b => String(b._id) === String(artefact.brand_id || artefact.brandId))?.name || `#${artefact.brand_id || artefact.brandId}`} +
+{new Date(artefact.CreatedAt).toLocaleDateString()}
+{t('artefacts.post')} #{artefact.post_id || artefact.postId}
+If you received this, email delivery is working correctly.
', + text: 'If you received this, email delivery is working correctly.', + }); + res.json({ success: true, to, messageId: info?.messageId, smtp: { host: config.host, port: config.port, from: config.from } }); + } catch (err) { + res.status(500).json({ success: false, error: err.message, code: err.code, smtp: { host: config.host, port: config.port } }); + } +}); + // ─── SETUP ROUTES ─────────────────────────────────────────────── app.get('/api/setup/status', async (req, res) => { @@ -4007,7 +4034,7 @@ app.get('/api/artefacts/:id', requireAuth, async (req, res) => { }); app.post('/api/artefacts', requireAuth, async (req, res) => { - const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids, copy_type } = req.body; + const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids, copy_type, post_id } = req.body; if (!title) return res.status(400).json({ error: 'Title is required' }); try { @@ -4018,6 +4045,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => { status: 'draft', content: content || null, brand_id: brand_id ? Number(brand_id) : null, + post_id: post_id ? Number(post_id) : null, project_id: project_id ? Number(project_id) : null, campaign_id: campaign_id ? Number(campaign_id) : null, approver_ids: approver_ids || null, @@ -4025,7 +4053,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => { created_by_user_id: req.session.userId, current_version: 1, }; - console.log('[POST /artefacts] Creating with:', JSON.stringify({ approver_ids: createData.approver_ids, project_id: createData.project_id, campaign_id: createData.campaign_id })); + console.log('[POST /artefacts] Creating with:', JSON.stringify({ post_id: createData.post_id, type: createData.type, copy_type: createData.copy_type, approver_ids: createData.approver_ids })); const created = await nocodb.create('Artefacts', createData); console.log('[POST /artefacts] NocoDB returned:', JSON.stringify({ Id: created.Id, approver_ids: created.approver_ids })); @@ -4039,7 +4067,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => { }); const artefact = await nocodb.get('Artefacts', created.Id); - console.log('[POST /artefacts] After re-read:', JSON.stringify({ Id: artefact.Id, approver_ids: artefact.approver_ids })); + console.log('[POST /artefacts] After re-read:', JSON.stringify({ Id: artefact.Id, post_id: artefact.post_id, copy_type: artefact.copy_type, approver_ids: artefact.approver_ids })); const approverIdList = artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []; const approvers = []; for (const id of approverIdList) { @@ -4267,6 +4295,45 @@ app.post('/api/artefacts/:id/link-post', requireAuth, async (req, res) => { // ─── ARTEFACT VERSIONS ────────────────────────────────────────── +// Creates the next working version automatically after rejection/revision. +// For copy artefacts, texts are copied from the last version so the creator +// doesn't start with a blank slate. +async function autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS) { + const versions = await nocodb.list('ArtefactVersions', { + where: `(artefact_id,eq,${artefact.Id})`, + sort: '-version_number', + limit: 1, + }); + const latest = versions[0]; + if (!latest) return; + + const nextNumber = latest.version_number + 1; + + const created = await nocodb.create('ArtefactVersions', { + artefact_id: artefact.Id, + version_number: nextNumber, + created_at: new Date().toISOString(), + notes: `Round ${nextNumber}`, + }); + + if (artefact.type === 'copy') { + const prevTexts = await nocodb.list('ArtefactVersionTexts', { + where: `(version_id,eq,${latest.Id})`, + limit: QUERY_LIMITS.large, + }); + for (const text of prevTexts) { + await nocodb.create('ArtefactVersionTexts', { + version_id: created.Id, + language_code: text.language_code, + language_label: text.language_label, + content: text.content, + }); + } + } + + await nocodb.update('Artefacts', artefact.Id, { current_version: nextNumber }); +} + // List all versions for an artefact app.get('/api/artefacts/:id/versions', requireAuth, async (req, res) => { try { @@ -4466,6 +4533,31 @@ app.post('/api/artefacts/:id/versions/:versionId/texts', requireAuth, async (req } }); +// Update language entry content +app.patch('/api/artefact-version-texts/:id', requireAuth, async (req, res) => { + try { + const text = await nocodb.get('ArtefactVersionTexts', req.params.id); + if (!text) return res.status(404).json({ error: 'Text not found' }); + + const version = await nocodb.get('ArtefactVersions', text.version_id); + const artefact = await nocodb.get('Artefacts', version.artefact_id); + + if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) { + return res.status(403).json({ error: 'You can only manage texts for your own artefacts' }); + } + + const { content } = req.body; + if (content === undefined) return res.status(400).json({ error: 'content is required' }); + + await nocodb.update('ArtefactVersionTexts', req.params.id, { content }); + const updated = await nocodb.get('ArtefactVersionTexts', req.params.id); + res.json(updated); + } catch (err) { + console.error('Update text error:', err); + res.status(500).json({ error: 'Failed to update text' }); + } +}); + // Delete language entry app.delete('/api/artefact-version-texts/:id', requireAuth, async (req, res) => { try { @@ -4823,6 +4915,9 @@ app.post('/api/public/review/:token/reject', async (req, res) => { feedback: feedback || '', }); + // Auto-advance to next working version + await autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS); + res.json({ success: true, message: 'Artefact rejected' }); notify.notifyRejected({ type: 'artefact', record: artefact, approverName: approved_by_name, feedback }); } catch (err) { @@ -4853,6 +4948,9 @@ app.post('/api/public/review/:token/revision', async (req, res) => { feedback: feedback || '', }); + // Auto-advance to next working version + await autoAdvanceArtefactVersion(artefact, nocodb, QUERY_LIMITS); + res.json({ success: true, message: 'Revision requested' }); notify.notifyRevisionRequested({ record: artefact, approverName: approved_by_name, feedback }); } catch (err) {