From 82236ecffa086f987df165311f15f71ad3a6582b Mon Sep 17 00:00:00 2001 From: fahed Date: Thu, 5 Mar 2026 14:17:16 +0300 Subject: [PATCH] feat: post approval workflow, i18n completion, and multiple fixes - Add approval process to posts (approver multi-select, rejected status column) - Reorganize PostDetailPanel into Content, Scheduling, Approval sections - Fix save button visibility: move to fixed footer via SlidePanel footer prop - Change date picker from datetime-local to date-only - Complete Arabic translations across all panels (Header, Issues, Artefacts) - Fix artefact versioning to start empty (copyFromPrevious defaults to false) - Separate media uploads by type (image, audio, video) in PostDetailPanel - Fix team membership save when editing own profile as superadmin - Server: add approver_ids column to Posts, enrich GET/POST/PATCH responses Co-Authored-By: Claude Opus 4.6 --- client/src/components/ArtefactDetailPanel.jsx | 152 ++++---- client/src/components/Header.jsx | 66 ++-- client/src/components/IssueCard.jsx | 20 +- client/src/components/IssueDetailPanel.jsx | 78 ++-- client/src/components/PostDetailPanel.jsx | 354 +++++++++++++----- client/src/components/SlidePanel.jsx | 3 +- client/src/i18n/ar.json | 197 +++++++++- client/src/i18n/en.json | 197 +++++++++- client/src/pages/Issues.jsx | 64 ++-- client/src/pages/PostProduction.jsx | 8 +- client/src/pages/Team.jsx | 2 +- server/server.js | 50 ++- 12 files changed, 882 insertions(+), 309 deletions(-) diff --git a/client/src/components/ArtefactDetailPanel.jsx b/client/src/components/ArtefactDetailPanel.jsx index a1f183d..b42b10e 100644 --- a/client/src/components/ArtefactDetailPanel.jsx +++ b/client/src/components/ArtefactDetailPanel.jsx @@ -65,7 +65,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel // New version modal const [showNewVersionModal, setShowNewVersionModal] = useState(false) const [newVersionNotes, setNewVersionNotes] = useState('') - const [copyFromPrevious, setCopyFromPrevious] = useState(true) + const [copyFromPrevious, setCopyFromPrevious] = useState(false) const [creatingVersion, setCreatingVersion] = useState(false) // File upload (for design/video) @@ -109,7 +109,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel } } catch (err) { console.error('Failed to load versions:', err) - toast.error('Failed to load versions') + toast.error(t('artefacts.failedLoadVersions')) } finally { setLoading(false) } @@ -126,7 +126,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel setComments(commentsRes.data || commentsRes || []) } catch (err) { console.error('Failed to load version data:', err) - toast.error('Failed to load version data') + toast.error(t('artefacts.failedLoadVersionData')) } } @@ -143,15 +143,15 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel copy_from_previous: artefact.type === 'copy' ? copyFromPrevious : false, }) - toast.success('New version created') + toast.success(t('artefacts.versionCreated')) setShowNewVersionModal(false) setNewVersionNotes('') - setCopyFromPrevious(true) + setCopyFromPrevious(false) loadVersions() onUpdate() } catch (err) { console.error('Create version failed:', err) - toast.error('Failed to create version') + toast.error(t('artefacts.failedCreateVersion')) } finally { setCreatingVersion(false) } @@ -159,20 +159,20 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const handleAddLanguage = async () => { if (!languageForm.language_code || !languageForm.language_label || !languageForm.content) { - toast.error('All fields are required') + toast.error(t('artefacts.allFieldsRequired')) return } setSavingLanguage(true) try { await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/texts`, languageForm) - toast.success('Language added') + toast.success(t('artefacts.languageAdded')) setShowLanguageModal(false) setLanguageForm({ language_code: '', language_label: '', content: '' }) loadVersionData(selectedVersion.Id) } catch (err) { console.error('Add language failed:', err) - toast.error('Failed to add language') + toast.error(t('artefacts.failedAddLanguage')) } finally { setSavingLanguage(false) } @@ -181,10 +181,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const handleDeleteLanguage = async (textId) => { try { await api.delete(`/artefact-version-texts/${textId}`) - toast.success('Language deleted') + toast.success(t('artefacts.languageDeleted')) loadVersionData(selectedVersion.Id) } catch (err) { - toast.error('Failed to delete language') + toast.error(t('artefacts.failedDeleteLanguage')) } } @@ -197,11 +197,11 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const formData = new FormData() formData.append('file', file) await api.upload(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/attachments`, formData) - toast.success('File uploaded') + toast.success(t('artefacts.fileUploaded')) loadVersionData(selectedVersion.Id) } catch (err) { console.error('Upload failed:', err) - toast.error('Upload failed') + toast.error(t('artefacts.uploadFailed')) } finally { setUploading(false) } @@ -209,7 +209,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const handleAddDriveVideo = async () => { if (!driveUrl.trim()) { - toast.error('Please enter a Google Drive URL') + toast.error(t('artefacts.enterDriveUrl')) return } @@ -218,13 +218,13 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/attachments`, { drive_url: driveUrl, }) - toast.success('Video link added') + toast.success(t('artefacts.videoLinkAdded')) setShowVideoModal(false) setDriveUrl('') loadVersionData(selectedVersion.Id) } catch (err) { console.error('Add Drive link failed:', err) - toast.error('Failed to add video link') + toast.error(t('artefacts.failedAddVideoLink')) } finally { setUploading(false) } @@ -233,10 +233,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const handleDeleteAttachment = async (attId) => { try { await api.delete(`/artefact-attachments/${attId}`) - toast.success('Attachment deleted') + toast.success(t('artefacts.attachmentDeleted')) loadVersionData(selectedVersion.Id) } catch (err) { - toast.error('Failed to delete attachment') + toast.error(t('artefacts.failedDeleteAttachment')) } } @@ -245,10 +245,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel try { const res = await api.post(`/artefacts/${artefact.Id}/submit-review`) setReviewUrl(res.reviewUrl || res.data?.reviewUrl || '') - toast.success('Submitted for review!') + toast.success(t('artefacts.submittedForReview')) onUpdate() } catch (err) { - toast.error('Failed to submit for review') + toast.error(t('artefacts.failedSubmitReview')) } finally { setSubmitting(false) } @@ -257,7 +257,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const copyReviewLink = () => { navigator.clipboard.writeText(reviewUrl) setCopied(true) - toast.success('Link copied to clipboard') + toast.success(t('artefacts.linkCopied')) setTimeout(() => setCopied(false), 2000) } @@ -269,11 +269,11 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/comments`, { content: newComment.trim(), }) - toast.success('Comment added') + toast.success(t('artefacts.commentAdded')) setNewComment('') loadVersionData(selectedVersion.Id) } catch (err) { - toast.error('Failed to add comment') + toast.error(t('artefacts.failedAddComment')) } finally { setAddingComment(false) } @@ -282,16 +282,16 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const handleUpdateField = async (field, value) => { try { await api.patch(`/artefacts/${artefact.Id}`, { [field]: value || null }) - toast.success('Updated') + toast.success(t('artefacts.updated')) onUpdate() } catch (err) { - toast.error('Failed to update') + toast.error(t('artefacts.failedUpdate')) } } const handleSaveDraft = async () => { if (!editTitle.trim()) { - toast.error('Title is required') + toast.error(t('artefacts.titleRequired')) return } setSavingDraft(true) @@ -300,10 +300,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel title: editTitle.trim(), description: editDescription.trim() || null, }) - toast.success('Draft saved') + toast.success(t('artefacts.draftSaved')) onUpdate() } catch (err) { - toast.error('Failed to save draft') + toast.error(t('artefacts.failedSaveDraft')) } finally { setSavingDraft(false) } @@ -314,7 +314,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel try { await onDelete(artefact.Id || artefact.id || artefact._id) } catch (err) { - toast.error('Failed to delete') + toast.error(t('artefacts.failedDelete')) setDeleting(false) } } @@ -377,17 +377,17 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel onClick={handleSaveDraft} disabled={savingDraft} className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light disabled:opacity-50 transition-colors" - title="Save draft" + title={t('artefacts.saveDraftTooltip')} > - {savingDraft ? 'Saving...' : 'Save'} + {savingDraft ? t('artefacts.savingDraft') : t('artefacts.saveDraft')} {onDelete && ( @@ -399,13 +399,13 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
{/* Description */}
-

Description

+

{t('artefacts.descriptionLabel')}