diff --git a/client/src/components/PostDetailPanel.jsx b/client/src/components/PostDetailPanel.jsx index 65daf5d..f06ec24 100644 --- a/client/src/components/PostDetailPanel.jsx +++ b/client/src/components/PostDetailPanel.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react' -import { X, Trash2, Upload, FileText, Link2, ExternalLink, FolderOpen, Image as ImageIcon, Music, Film } from 'lucide-react' +import { X, Trash2, Upload, FileText, Link2, ExternalLink, FolderOpen, Image as ImageIcon, Music, Film, Send, CheckCircle2, XCircle } from 'lucide-react' import { useLanguage } from '../i18n/LanguageContext' import { api, PLATFORMS, getBrandColor } from '../utils/api' import ApproverMultiSelect from './ApproverMultiSelect' @@ -124,6 +124,18 @@ export default function PostDetailPanel({ post, onClose, onSave, onDelete, brand } } + const handleStatusAction = async (newStatus) => { + if (!postId || saving) return + setSaving(true) + try { + await onSave(postId, { ...form, status: newStatus, approver_ids: (form.approver_ids || []).length > 0 ? form.approver_ids.join(',') : null }) + setForm(f => ({ ...f, status: newStatus })) + setDirty(false) + } finally { + setSaving(false) + } + } + const confirmDelete = async () => { setShowDeleteConfirm(false) await onDelete(postId) @@ -380,13 +392,59 @@ export default function PostDetailPanel({ post, onClose, onSave, onDelete, brand {/* Approval Section */} -
- - update('approver_ids', ids)} - /> +
+
+ + update('approver_ids', ids)} + /> +
+ + {!isCreateMode && ( +
+ {(form.status === 'draft' || form.status === 'rejected') && ( + + )} + {form.status === 'in_review' && ( + <> + + + + )} + {form.status === 'approved' && ( + + )} +
+ )}
diff --git a/client/src/i18n/ar.json b/client/src/i18n/ar.json index ad4e89a..4c4db22 100644 --- a/client/src/i18n/ar.json +++ b/client/src/i18n/ar.json @@ -888,5 +888,6 @@ "posts.approvers": "المعتمدون", "posts.selectApprovers": "اختر المعتمدين...", "posts.scheduling": "الجدولة والتعيين", - "posts.content": "المحتوى" + "posts.content": "المحتوى", + "posts.reject": "رفض" } \ No newline at end of file diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index 7b31aac..58792ef 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -888,5 +888,6 @@ "posts.approvers": "Approvers", "posts.selectApprovers": "Select approvers...", "posts.scheduling": "Scheduling & Assignment", - "posts.content": "Content" + "posts.content": "Content", + "posts.reject": "Reject" } \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 5273e5e..5377721 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -421,6 +421,10 @@ textarea { overflow: hidden; } +.collapsible-content.is-open > .collapsible-inner { + overflow: visible; +} + /* Stagger children */ .stagger-children > * { opacity: 0; diff --git a/server/sync-brands.js b/server/sync-brands.js new file mode 100644 index 0000000..26a68d7 --- /dev/null +++ b/server/sync-brands.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/** + * Sync brands from local NocoDB to a remote NocoDB instance. + * + * Usage: + * REMOTE_NOCODB_URL=https://... REMOTE_NOCODB_TOKEN=... REMOTE_NOCODB_BASE_ID=... node sync-brands.js + * + * Reads local config from .env (NOCODB_URL, NOCODB_TOKEN, NOCODB_BASE_ID). + * Syncs all brands from local → remote (upserts by name). + */ + +import 'dotenv/config' + +const LOCAL_URL = process.env.NOCODB_URL || 'http://localhost:8090' +const LOCAL_TOKEN = process.env.NOCODB_TOKEN +const LOCAL_BASE_ID = process.env.NOCODB_BASE_ID + +const REMOTE_URL = process.env.REMOTE_NOCODB_URL +const REMOTE_TOKEN = process.env.REMOTE_NOCODB_TOKEN +const REMOTE_BASE_ID = process.env.REMOTE_NOCODB_BASE_ID + +if (!REMOTE_URL || !REMOTE_TOKEN || !REMOTE_BASE_ID) { + console.error('Missing env vars: REMOTE_NOCODB_URL, REMOTE_NOCODB_TOKEN, REMOTE_NOCODB_BASE_ID') + process.exit(1) +} + +async function resolveTableId(baseUrl, token, baseId, tableName) { + const res = await fetch(`${baseUrl}/api/v2/meta/bases/${baseId}/tables`, { + headers: { 'xc-token': token }, + }) + const data = await res.json() + const table = (data.list || []).find(t => t.title === tableName) + if (!table) throw new Error(`Table "${tableName}" not found`) + return table.id +} + +async function listRecords(baseUrl, token, tableId) { + const res = await fetch(`${baseUrl}/api/v2/tables/${tableId}/records?limit=200`, { + headers: { 'xc-token': token }, + }) + const data = await res.json() + return data.list || [] +} + +async function createRecord(baseUrl, token, tableId, record) { + const res = await fetch(`${baseUrl}/api/v2/tables/${tableId}/records`, { + method: 'POST', + headers: { 'xc-token': token, 'Content-Type': 'application/json' }, + body: JSON.stringify(record), + }) + return res.json() +} + +async function updateRecord(baseUrl, token, tableId, record) { + const res = await fetch(`${baseUrl}/api/v2/tables/${tableId}/records`, { + method: 'PATCH', + headers: { 'xc-token': token, 'Content-Type': 'application/json' }, + body: JSON.stringify(record), + }) + return res.json() +} + +async function main() { + console.log('Resolving table IDs...') + const localTableId = await resolveTableId(LOCAL_URL, LOCAL_TOKEN, LOCAL_BASE_ID, 'Brands') + const remoteTableId = await resolveTableId(REMOTE_URL, REMOTE_TOKEN, REMOTE_BASE_ID, 'Brands') + + console.log('Fetching local brands...') + const localBrands = await listRecords(LOCAL_URL, LOCAL_TOKEN, localTableId) + console.log(` Found ${localBrands.length} local brands`) + + console.log('Fetching remote brands...') + const remoteBrands = await listRecords(REMOTE_URL, REMOTE_TOKEN, remoteTableId) + console.log(` Found ${remoteBrands.length} remote brands`) + + const remoteByName = new Map(remoteBrands.map(b => [b.name, b])) + + const FIELDS = ['name', 'name_ar', 'priority', 'color', 'icon', 'category', 'logo'] + + let created = 0, updated = 0, skipped = 0 + + for (const brand of localBrands) { + const data = {} + for (const f of FIELDS) { + if (brand[f] !== undefined && brand[f] !== null) data[f] = brand[f] + } + + const existing = remoteByName.get(brand.name) + if (existing) { + // Check if anything changed + const needsUpdate = FIELDS.some(f => brand[f] !== existing[f]) + if (needsUpdate) { + await updateRecord(REMOTE_URL, REMOTE_TOKEN, remoteTableId, { Id: existing.Id, ...data }) + console.log(` Updated: ${brand.name}`) + updated++ + } else { + skipped++ + } + } else { + await createRecord(REMOTE_URL, REMOTE_TOKEN, remoteTableId, data) + console.log(` Created: ${brand.name}`) + created++ + } + } + + console.log(`\nDone! Created: ${created}, Updated: ${updated}, Skipped (no changes): ${skipped}`) +} + +main().catch(err => { + console.error('Sync failed:', err) + process.exit(1) +})