diff --git a/client/index.html b/client/index.html index dc9efea..f331249 100644 --- a/client/index.html +++ b/client/index.html @@ -7,7 +7,7 @@ - Digital Hub + Rawaj
diff --git a/client/src/App.jsx b/client/src/App.jsx index f6acf08..42dacd9 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -37,6 +37,7 @@ const PublicIssueSubmit = lazy(() => import('./pages/PublicIssueSubmit')) const PublicIssueTracker = lazy(() => import('./pages/PublicIssueTracker')) const Translations = lazy(() => import('./pages/Translations')) const PublicTranslationReview = lazy(() => import('./pages/PublicTranslationReview')) +const PublicBudgetApproval = lazy(() => import('./pages/PublicBudgetApproval')) const ForgotPassword = lazy(() => import('./pages/ForgotPassword')) const ResetPassword = lazy(() => import('./pages/ResetPassword')) @@ -161,7 +162,7 @@ function AppContent() { {/* Profile completion prompt */} {showProfilePrompt && ( -
+
⚠️ @@ -298,6 +299,7 @@ function AppContent() { } /> } /> } /> + } /> : }> } /> {hasModule('marketing') && <> diff --git a/client/src/components/ApproverMultiSelect.jsx b/client/src/components/ApproverMultiSelect.jsx index ffb9bfb..6f9758b 100644 --- a/client/src/components/ApproverMultiSelect.jsx +++ b/client/src/components/ApproverMultiSelect.jsx @@ -64,7 +64,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang ))} - +
{open && (
@@ -76,7 +76,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang key={uid} type="button" onClick={() => toggle(uid)} - className={`w-full text-left px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between ${ + className={`w-full text-start px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between ${ isSelected ? 'text-brand-primary font-medium' : 'text-text-primary' }`} > diff --git a/client/src/components/ArtefactDetailPanel.jsx b/client/src/components/ArtefactDetailPanel.jsx index 5c65623..d66a9e5 100644 --- a/client/src/components/ArtefactDetailPanel.jsx +++ b/client/src/components/ArtefactDetailPanel.jsx @@ -1,13 +1,13 @@ import { useState, useEffect, useContext } from 'react' -import { Plus, Copy, Check, ExternalLink, Upload, Globe, Trash2, FileText, Image as ImageIcon, Film, Sparkles, MessageSquare, Save, FileEdit, Layers, ShieldCheck } from 'lucide-react' +import { Copy, Check, ExternalLink, Trash2, FileText, Image as ImageIcon, Film, Sparkles, MessageSquare, Save, FileEdit, Layers, ShieldCheck } from 'lucide-react' import { AppContext } from '../App' import { useLanguage } from '../i18n/LanguageContext' import { api } from '../utils/api' import Modal from './Modal' import TabbedModal from './TabbedModal' import { useToast } from './ToastContainer' -import ArtefactVersionTimeline from './ArtefactVersionTimeline' import ApproverMultiSelect from './ApproverMultiSelect' +import { ArtefactDetailVersionsTab } from './ArtefactDetailVersionsTab' const STATUS_COLORS = { draft: 'bg-surface-tertiary text-text-secondary', @@ -17,13 +17,6 @@ const STATUS_COLORS = { revision_requested: 'bg-orange-100 text-orange-700', } -const AVAILABLE_LANGUAGES = [ - { code: 'AR', label: '\u0627\u0644\u0639\u0631\u0628\u064A\u0629' }, - { code: 'EN', label: 'English' }, - { code: 'FR', label: 'Fran\u00E7ais' }, - { code: 'ID', label: 'Bahasa Indonesia' }, -] - const TYPE_ICONS = { copy: FileText, design: ImageIcon, @@ -55,27 +48,10 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel const reviewUrl = freshReviewUrl || (artefact.approval_token ? `${window.location.origin}/review/${artefact.approval_token}` : '') const [savingDraft, setSavingDraft] = useState(false) const [deleting, setDeleting] = useState(false) - const [confirmDeleteLangId, setConfirmDeleteLangId] = useState(null) - const [confirmDeleteAttId, setConfirmDeleteAttId] = useState(null) const [showDeleteArtefactConfirm, setShowDeleteArtefactConfirm] = useState(false) - // Language management (for copy type) - const [showLanguageModal, setShowLanguageModal] = useState(false) - const [languageForm, setLanguageForm] = useState({ language_code: '', language_label: '', content: '' }) - const [savingLanguage, setSavingLanguage] = useState(false) - - // New version modal - const [showNewVersionModal, setShowNewVersionModal] = useState(false) - const [newVersionNotes, setNewVersionNotes] = useState('') - const [copyFromPrevious, setCopyFromPrevious] = useState(false) - const [creatingVersion, setCreatingVersion] = useState(false) - // File upload (for design/video) const [uploading, setUploading] = useState(false) - - // Video inline (Drive link input) - const [driveUrl, setDriveUrl] = useState('') - const [dragOver, setDragOver] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) // Comments @@ -137,57 +113,23 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel loadVersionData(version.Id) } - const handleCreateVersion = async () => { - setCreatingVersion(true) - try { - await api.post(`/artefacts/${artefact.Id}/versions`, { - notes: newVersionNotes || `Version ${(versions[versions.length - 1]?.version_number || 0) + 1}`, - copy_from_previous: artefact.type === 'copy' ? copyFromPrevious : false, - }) - - toast.success(t('artefacts.versionCreated')) - setShowNewVersionModal(false) - setNewVersionNotes('') - setCopyFromPrevious(false) - loadVersions() - onUpdate() - } catch (err) { - console.error('Create version failed:', err) - toast.error(t('artefacts.failedCreateVersion')) - } finally { - setCreatingVersion(false) - } + 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 () => { - if (!languageForm.language_code || !languageForm.language_label || !languageForm.content) { - toast.error(t('artefacts.allFieldsRequired')) - return - } - - setSavingLanguage(true) - try { - await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/texts`, languageForm) - 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(t('artefacts.failedAddLanguage')) - } finally { - setSavingLanguage(false) - } + const handleAddLanguage = async (languageForm) => { + await api.post(`/artefacts/${artefact.Id}/versions/${selectedVersion.Id}/texts`, languageForm) + toast.success(t('artefacts.languageAdded')) + loadVersionData(selectedVersion.Id) } const handleDeleteLanguage = async (textId) => { - try { - await api.delete(`/artefact-version-texts/${textId}`) - toast.success(t('artefacts.languageDeleted')) - loadVersionData(selectedVersion.Id) - } catch (err) { - toast.error(t('artefacts.failedDeleteLanguage')) - } + await api.delete(`/artefact-version-texts/${textId}`) + toast.success(t('artefacts.languageDeleted')) + loadVersionData(selectedVersion.Id) } const handleFileUpload = async (fileOrEvent) => { @@ -215,16 +157,7 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel } } - const handleVideoDrop = (e) => { - e.preventDefault() - setDragOver(false) - const file = e.dataTransfer.files?.[0] - if (file && file.type.startsWith('video/')) { - handleFileUpload(file) - } - } - - const handleAddDriveVideo = async () => { + const handleAddDriveVideo = async (driveUrl) => { if (!driveUrl.trim()) { toast.error(t('artefacts.enterDriveUrl')) return @@ -236,7 +169,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel drive_url: driveUrl, }) toast.success(t('artefacts.videoLinkAdded')) - setDriveUrl('') loadVersionData(selectedVersion.Id) } catch (err) { console.error('Add Drive link failed:', err) @@ -247,13 +179,9 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel } const handleDeleteAttachment = async (attId) => { - try { - await api.delete(`/artefact-attachments/${attId}`) - toast.success(t('artefacts.attachmentDeleted')) - loadVersionData(selectedVersion.Id) - } catch (err) { - toast.error(t('artefacts.failedDeleteAttachment')) - } + await api.delete(`/artefact-attachments/${attId}`) + toast.success(t('artefacts.attachmentDeleted')) + loadVersionData(selectedVersion.Id) } const handleSubmitReview = async () => { @@ -501,213 +429,22 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel {/* Versions Tab */} {activeTab === 'versions' && ( -
- {/* Version Timeline */} -
-
-

{t('artefacts.versions')}

- -
- -
- - {/* Type-specific content */} - {versionData && selectedVersion && ( -
- {/* COPY TYPE: Language entries */} - {artefact.type === 'copy' && ( -
-
-

{t('artefacts.languages')}

- -
- - {versionData.texts && versionData.texts.length > 0 ? ( -
- {versionData.texts.map(text => ( -
-
-
- - {text.language_code} - - {text.language_label} -
- -
-
- {text.content} -
-
- ))} -
- ) : ( -
- -

{t('artefacts.noLanguages')}

-
- )} -
- )} - - {/* DESIGN TYPE: Image gallery */} - {artefact.type === 'design' && ( -
-
-

{t('artefacts.imagesLabel')}

- -
- - {versionData.attachments && versionData.attachments.length > 0 ? ( -
- {versionData.attachments.map(att => ( -
- {att.original_name} -
- -
-
- {att.original_name} -
-
- ))} -
- ) : ( -
- -

{t('artefacts.noImages')}

-
- )} -
- )} - - {/* VIDEO TYPE: Files and Drive links — all inline */} - {artefact.type === 'video' && ( -
-

{t('artefacts.videosLabel')}

- - {/* Existing attachments */} - {versionData.attachments && versionData.attachments.length > 0 && ( -
- {versionData.attachments.map(att => ( -
- {att.drive_url ? ( -
-
- {t('artefacts.googleDriveVideo')} - -
-