feat: comprehensive UI overhaul + budget allocation redesign
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>
This commit is contained in:
+23
-23
@@ -325,16 +325,16 @@ export default function Tasks() {
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-tertiary" />
|
||||
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder={t('tasks.search')}
|
||||
className="w-full pl-9 pr-3 py-1.5 text-sm border border-border rounded-lg bg-surface-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
||||
className="w-full ps-9 pe-3 py-1.5 text-sm border border-border rounded-lg bg-surface-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => setSearchQuery('')} className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-text-tertiary hover:text-text-primary">
|
||||
<button onClick={() => setSearchQuery('')} className="absolute end-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-text-tertiary hover:text-text-primary">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
@@ -350,7 +350,7 @@ export default function Tasks() {
|
||||
onClick={() => setViewMode(mode)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||
viewMode === mode
|
||||
? 'bg-white text-text-primary shadow-sm'
|
||||
? 'bg-surface text-text-primary shadow-sm'
|
||||
: 'text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
@@ -399,7 +399,7 @@ export default function Tasks() {
|
||||
<select
|
||||
value={filterProject}
|
||||
onChange={e => setFilterProject(e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('tasks.allProjects')}</option>
|
||||
{taskProjects.map(p => (
|
||||
@@ -411,7 +411,7 @@ export default function Tasks() {
|
||||
<select
|
||||
value={filterBrand}
|
||||
onChange={e => setFilterBrand(e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('tasks.allBrands')}</option>
|
||||
{taskBrands.map(b => (
|
||||
@@ -440,7 +440,7 @@ export default function Tasks() {
|
||||
className={`px-2.5 py-1 text-[11px] font-medium rounded-full border transition-colors ${
|
||||
active
|
||||
? 'bg-brand-primary/10 border-brand-primary/20 text-brand-primary'
|
||||
: 'bg-white border-border text-text-tertiary'
|
||||
: 'bg-surface border-border text-text-tertiary'
|
||||
}`}
|
||||
>
|
||||
{t(`tasks.${s}`)}
|
||||
@@ -453,7 +453,7 @@ export default function Tasks() {
|
||||
<select
|
||||
value={filterPriority}
|
||||
onChange={e => setFilterPriority(e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('tasks.allPriorities')}</option>
|
||||
<option value="low">{t('tasks.priority.low')}</option>
|
||||
@@ -466,7 +466,7 @@ export default function Tasks() {
|
||||
<select
|
||||
value={filterAssignee}
|
||||
onChange={e => setFilterAssignee(e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('tasks.allAssignees')}</option>
|
||||
{(assignableUsers || []).map(m => (
|
||||
@@ -479,7 +479,7 @@ export default function Tasks() {
|
||||
<select
|
||||
value={filterCreator}
|
||||
onChange={e => setFilterCreator(e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2.5 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('tasks.allCreators')}</option>
|
||||
{users.map(m => (
|
||||
@@ -501,7 +501,7 @@ export default function Tasks() {
|
||||
type="date"
|
||||
value={filterDateFrom}
|
||||
onChange={e => { setFilterDateFrom(e.target.value); setActivePreset('') }}
|
||||
className="px-2 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
title={t('posts.periodFrom')}
|
||||
/>
|
||||
<span className="text-text-tertiary text-xs">-</span>
|
||||
@@ -509,7 +509,7 @@ export default function Tasks() {
|
||||
type="date"
|
||||
value={filterDateTo}
|
||||
onChange={e => { setFilterDateTo(e.target.value); setActivePreset('') }}
|
||||
className="px-2 py-1.5 text-xs border border-border rounded-lg bg-white text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
className="px-2 py-1.5 text-xs border border-border rounded-lg bg-surface text-text-primary focus:outline-none focus:ring-1 focus:ring-brand-primary/20"
|
||||
title={t('posts.periodTo')}
|
||||
/>
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ export default function Tasks() {
|
||||
className={`px-2.5 py-1 text-[11px] font-medium rounded-full border transition-colors ${
|
||||
filterOverdue
|
||||
? 'bg-red-50 border-red-200 text-red-600'
|
||||
: 'bg-white border-border text-text-tertiary'
|
||||
: 'bg-surface border-border text-text-tertiary'
|
||||
}`}
|
||||
>
|
||||
{t('tasks.overdue')}
|
||||
@@ -599,7 +599,7 @@ export default function Tasks() {
|
||||
onDelete={() => setShowBulkDeleteConfirm(true)}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
<div className="bg-surface rounded-xl border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-secondary/50">
|
||||
@@ -614,28 +614,28 @@ export default function Tasks() {
|
||||
</th>
|
||||
<th className="w-8 px-3 py-2.5"></th>
|
||||
<th
|
||||
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
onClick={() => toggleSort('title')}
|
||||
>
|
||||
{t('tasks.taskTitle')} {sortBy === 'title' && (sortDir === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.project')}</th>
|
||||
<th className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.brand')}</th>
|
||||
<th className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.project')}</th>
|
||||
<th className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.brand')}</th>
|
||||
<th
|
||||
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
onClick={() => toggleSort('status')}
|
||||
>
|
||||
{t('tasks.status')} {sortBy === 'status' && (sortDir === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.assignee')}</th>
|
||||
<th className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase">{t('tasks.assignee')}</th>
|
||||
<th
|
||||
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
onClick={() => toggleSort('due_date')}
|
||||
>
|
||||
{t('tasks.dueDate')} {sortBy === 'due_date' && (sortDir === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
className="text-start px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
onClick={() => toggleSort('priority')}
|
||||
>
|
||||
{t('tasks.priority')} {sortBy === 'priority' && (sortDir === 'asc' ? '↑' : '↓')}
|
||||
@@ -651,7 +651,7 @@ export default function Tasks() {
|
||||
const brandName = task.brand_name || task.brandName
|
||||
const assignedName = task.assigned_name || task.assignedName
|
||||
const statusLabels = { todo: t('tasks.todo'), in_progress: t('tasks.in_progress'), done: t('tasks.done') }
|
||||
const statusColors = { todo: 'bg-gray-100 text-gray-600', in_progress: 'bg-blue-100 text-blue-700', done: 'bg-emerald-100 text-emerald-700' }
|
||||
const statusColors = { todo: 'bg-gray-100 text-text-secondary', in_progress: 'bg-blue-100 text-blue-700', done: 'bg-emerald-100 text-emerald-700' }
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -675,7 +675,7 @@ export default function Tasks() {
|
||||
{task.title}
|
||||
</span>
|
||||
{(task.comment_count || task.commentCount) > 0 && (
|
||||
<span className="ml-2 text-[10px] text-text-tertiary">💬 {task.comment_count || task.commentCount}</span>
|
||||
<span className="ms-2 text-[10px] text-text-tertiary">💬 {task.comment_count || task.commentCount}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-text-tertiary text-xs">{projectName || '—'}</td>
|
||||
|
||||
Reference in New Issue
Block a user