feat: post composition redesign + budget allocation + brand identity (Rawaj)
Post Workflow: - PostDetail full page (/posts/:id) replaces slide panel approach - Post = 1 Caption Copy + 1 Body Copy + 1 Design + 0-1 Video - copy_type field on Translations (caption/body) - Composition endpoint returns rich data (content preview, languages, thumbnails) - Stage auto-advances on translation/artefact changes (both link and unlink) - "Translations" renamed to "Copy" in navigation - GET /api/posts/:id, /api/translations/:id, /api/artefacts/:id routes added - PostProduction: "New Post" creates → navigates to full page - CampaignDetail: click post → navigates to full page - Inline link picker (no modals) with search + rich item display - PostComposition sub-components for caption, copy, designs, video, formats, readiness Budget Allocation: - Single source of truth: BudgetEntries (Campaign.budget deprecated) - Budget mutex for race conditions - Validation at all levels (main → campaign → track, expenses) - CEO approval workflow: BudgetRequests table, public approval page - Finance page: request budget UI, budget requests section - Settings: CEO email field - All emails branded with "Rawaj —" prefix Brand Identity: - Name: Rawaj (رواج) — trending/virality - Deep teal palette (#0d9488), forest-tinted dark mode - DM Sans font, custom SVG logo - Consistent across login, sidebar, emails, public pages Approval Workflow: - Single reviewer per artefact (not multi-select) - Reviewer redirect on public review page - Server blocks submit-review without reviewer - Review URLs use APP_URL (not server URL) UI/UX: - Scroll clipping fix: Modal, TabbedModal, SlidePanel restructured to avoid overflow-y-auto clipping native select dropdowns - section-card overflow-hidden → overflow-clip - All page titles via Header.jsx (removed duplicate h1s) - CampaignDetail redesigned: prominent budget card, compact team Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
const nocodb = require('./nocodb');
|
||||
|
||||
async function getPostComposition(postId) {
|
||||
const post = await nocodb.get('Posts', postId);
|
||||
if (!post) return null;
|
||||
|
||||
const translations = await nocodb.list('Translations', {
|
||||
where: `(post_id,eq,${postId})`, limit: 100,
|
||||
});
|
||||
const caption = translations.find(t => t.copy_type === 'caption') || null;
|
||||
const bodyCopy = translations.find(t => t.copy_type === 'body' || !t.copy_type) || null;
|
||||
|
||||
const artefacts = await nocodb.list('Artefacts', {
|
||||
where: `(post_id,eq,${postId})`, limit: 100,
|
||||
});
|
||||
const design = artefacts.find(a => (a.type || 'design') === 'design') || null;
|
||||
const video = artefacts.find(a => a.type === 'video') || null;
|
||||
|
||||
let platforms = [];
|
||||
try { platforms = JSON.parse(post.platforms || '[]'); } catch { platforms = post.platform ? [post.platform] : []; }
|
||||
|
||||
const waitingOn = [];
|
||||
if (caption && caption.status !== 'approved') waitingOn.push('Caption');
|
||||
if (bodyCopy && bodyCopy.status !== 'approved') waitingOn.push('Copy');
|
||||
if (design && design.status !== 'approved') waitingOn.push('Design');
|
||||
if (video && video.status !== 'approved') waitingOn.push('Video');
|
||||
|
||||
const hasPieces = caption || bodyCopy || design || video;
|
||||
const piecesReady = hasPieces && waitingOn.length === 0;
|
||||
|
||||
// Get translation texts for languages preview
|
||||
const getTexts = async (translationId) => {
|
||||
try {
|
||||
const texts = await nocodb.list('TranslationTexts', { where: `(translation_id,eq,${translationId})`, limit: 20 });
|
||||
return texts.map(tt => ({ language: tt.language_code || tt.language, status: tt.status || 'draft' }));
|
||||
} catch { return []; }
|
||||
};
|
||||
const captionTexts = caption ? await getTexts(caption.Id) : [];
|
||||
const bodyTexts = bodyCopy ? await getTexts(bodyCopy.Id) : [];
|
||||
|
||||
// Get first attachment for design/video thumbnail
|
||||
const getFirstAttachment = async (artefactId) => {
|
||||
try {
|
||||
const versions = await nocodb.list('ArtefactVersions', { where: `(artefact_id,eq,${artefactId})`, sort: '-version_number', limit: 1 });
|
||||
if (versions.length === 0) return null;
|
||||
const attachments = await nocodb.list('ArtefactAttachments', { where: `(version_id,eq,${versions[0].Id})`, limit: 1 });
|
||||
return attachments.length > 0 ? (attachments[0].url || attachments[0].file_url || null) : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
const designThumb = design ? (design.thumbnail_url || await getFirstAttachment(design.Id)) : null;
|
||||
const videoThumb = video ? (video.thumbnail_url || await getFirstAttachment(video.Id)) : null;
|
||||
|
||||
return {
|
||||
caption: caption ? { id: caption.Id, title: caption.title, status: caption.status, language: caption.source_language, content_preview: (caption.source_content || '').slice(0, 120), languages: captionTexts } : null,
|
||||
body_copy: bodyCopy ? { id: bodyCopy.Id, title: bodyCopy.title, status: bodyCopy.status, language: bodyCopy.source_language, content_preview: (bodyCopy.source_content || '').slice(0, 120), languages: bodyTexts } : null,
|
||||
design: design ? { id: design.Id, title: design.title, status: design.status, thumbnail_url: designThumb, current_version: design.current_version } : null,
|
||||
video: video ? { id: video.Id, title: video.title, status: video.status, thumbnail_url: videoThumb, current_version: video.current_version } : null,
|
||||
platforms,
|
||||
pieces_ready: piecesReady,
|
||||
waiting_on: waitingOn,
|
||||
stage: post.stage || 'copy',
|
||||
};
|
||||
}
|
||||
|
||||
function computeStage(composition) {
|
||||
const { caption, body_copy, design, video, pieces_ready } = composition;
|
||||
if (pieces_ready) return 'post';
|
||||
if (design || video) return 'design';
|
||||
// Check if we have any copy at all
|
||||
const hasCopy = caption || body_copy;
|
||||
if (!hasCopy) return 'copy';
|
||||
return 'copy';
|
||||
}
|
||||
|
||||
module.exports = { getPostComposition, computeStage };
|
||||
Reference in New Issue
Block a user