e1d1c392eb
Audit & Quality: - RTL: replaced 121 LTR-only utilities (text-left, pl-, left-) with logical properties - A11y: focus traps + ARIA on Modal/SlidePanel/TabbedModal, clickable divs→buttons - Theming: bg-white→bg-surface (170 instances), text-gray→semantic tokens - Performance: useMemo on filters, loading="lazy" on 24 images - CSS: prefers-reduced-motion, removed dead animations Component Splits: - PostDetailPanel: 1332→623 lines + 4 sub-components - ArtefactDetailPanel: 972→590 lines + 1 sub-component Brand Identity — Rawaj (رواج): - New name, DM Sans font, deep teal palette (#0d9488) - Custom SVG logo, forest-tinted dark mode - All emails branded with app name in subject line Design Refinement: - Dashboard: merged posts+deadlines into tabbed ActivityFeed, inline stats - Quieter: removed card lift, brand glow, gradient text, mesh backgrounds - CampaignDetail: prominent budget card, compact team avatars, Lucide icons - Consistent page titles via Header.jsx, standardized section headers - Finance page fully i18n'd (20+ hardcoded strings replaced) Budget Allocation Redesign: - Single source of truth: BudgetEntries (Campaign.budget deprecated) - Validation at all levels: main→campaign→track, expenses blocked if insufficient - Budget request workflow with CEO approval via public link - BudgetRequests table, CRUD routes, public approval page - Budget mutex for race condition prevention - Idempotent migration for existing campaign budgets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
4.7 KiB
React
110 lines
4.7 KiB
React
import { Send, CheckCircle2, XCircle, Copy, Check, Clock } from 'lucide-react'
|
|
import { useLanguage } from '../i18n/LanguageContext'
|
|
import ApproverMultiSelect from './ApproverMultiSelect'
|
|
|
|
export function PostDetailApproval({
|
|
form,
|
|
update,
|
|
post,
|
|
isCreateMode,
|
|
reviewUrl,
|
|
copied,
|
|
submittingReview,
|
|
saving,
|
|
teamMembers,
|
|
onSubmitReview,
|
|
onCopyReviewLink,
|
|
onStatusAction,
|
|
}) {
|
|
const { t } = useLanguage()
|
|
|
|
return (
|
|
<div className="p-6 space-y-5 w-full">
|
|
<div className="bg-surface-secondary rounded-xl p-4">
|
|
<label className="block text-xs font-medium text-text-primary mb-2">{t('posts.approvers')}</label>
|
|
<ApproverMultiSelect
|
|
users={teamMembers || []}
|
|
selected={form.approver_ids || []}
|
|
onChange={ids => update('approver_ids', ids)}
|
|
/>
|
|
</div>
|
|
|
|
{!isCreateMode && (
|
|
<div className="space-y-4">
|
|
{/* Approval status cards */}
|
|
{form.status === 'approved' && post.approved_by_name && (
|
|
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-emerald-100 flex items-center justify-center shrink-0 mt-0.5">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-emerald-800">{t('posts.approvedBy')} {post.approved_by_name}</p>
|
|
{post.feedback && <p className="text-xs text-emerald-700 mt-1.5 leading-relaxed">{post.feedback}</p>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{form.status === 'rejected' && post.approved_by_name && (
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0 mt-0.5">
|
|
<XCircle className="w-4 h-4 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-red-800">{t('posts.rejectedBy')} {post.approved_by_name}</p>
|
|
{post.feedback && <p className="text-xs text-red-700 mt-1.5 leading-relaxed">{post.feedback}</p>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{form.status === 'in_review' && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-center">
|
|
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center mx-auto mb-2">
|
|
<Clock className="w-5 h-5 text-amber-600" />
|
|
</div>
|
|
<p className="text-sm font-semibold text-amber-800">{t('posts.awaitingReview')}</p>
|
|
<p className="text-xs text-amber-600 mt-1">{t('posts.awaitingReviewDesc')}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Review link */}
|
|
{reviewUrl && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<div className="text-xs font-semibold text-blue-900 mb-2.5">{t('posts.reviewLinkTitle')}</div>
|
|
<div className="flex items-center gap-2">
|
|
<input type="text" value={reviewUrl} readOnly className="flex-1 px-3 py-2 text-xs bg-surface border border-blue-200 rounded-lg font-mono" />
|
|
<button onClick={onCopyReviewLink} className="p-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm">
|
|
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex gap-3">
|
|
{!reviewUrl && (
|
|
<button
|
|
onClick={onSubmitReview}
|
|
disabled={submittingReview}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors font-medium text-sm disabled:opacity-50 shadow-sm"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
{submittingReview ? t('posts.submitting') : t('posts.sendToReview')}
|
|
</button>
|
|
)}
|
|
|
|
{form.status === 'approved' && (
|
|
<button
|
|
onClick={() => onStatusAction('scheduled')}
|
|
disabled={saving}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 transition-colors font-medium text-sm disabled:opacity-50 shadow-sm"
|
|
>
|
|
{t('posts.schedule')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|