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:
@@ -223,14 +223,14 @@ export default function ProjectDetail() {
|
||||
</button>
|
||||
|
||||
{/* Project header */}
|
||||
<div className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
<div className="bg-surface rounded-xl border border-border overflow-hidden">
|
||||
{/* Thumbnail banner */}
|
||||
{(project.thumbnail_url || project.thumbnailUrl) && (
|
||||
<div className="relative w-full h-40 overflow-hidden">
|
||||
<img src={project.thumbnail_url || project.thumbnailUrl} alt="" className="w-full h-full object-cover" />
|
||||
<img src={project.thumbnail_url || project.thumbnailUrl} alt="" className="w-full h-full object-cover" loading="lazy" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
||||
{canEditProject && (
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1">
|
||||
<div className="absolute top-2 end-2 flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => thumbnailInputRef.current?.click()}
|
||||
className="px-2 py-1 text-xs bg-black/40 hover:bg-black/60 rounded text-white transition-colors"
|
||||
@@ -341,7 +341,7 @@ export default function ProjectDetail() {
|
||||
key={v.id}
|
||||
onClick={() => setView(v.id)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
view === v.id ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary hover:text-text-secondary'
|
||||
view === v.id ? 'bg-surface shadow-sm text-text-primary' : 'text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
<v.icon className="w-4 h-4" />
|
||||
@@ -411,21 +411,21 @@ export default function ProjectDetail() {
|
||||
|
||||
{/* ─── LIST VIEW ─── */}
|
||||
{view === 'list' && (
|
||||
<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">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-secondary">
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider w-8"></th>
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Task</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Status</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Priority</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Assignee</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Due</th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider w-8"></th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Task</th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Status</th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Priority</th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Assignee</th>
|
||||
<th className="text-start px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-light">
|
||||
{tasks.length === 0 ? (
|
||||
<tr><td colSpan={6} className="py-12 text-center text-sm text-text-tertiary">No tasks yet</td></tr>
|
||||
<tr><td colSpan={6} className="py-12 text-center text-sm text-text-tertiary">{t('tasks.noTasks')}</td></tr>
|
||||
) : (
|
||||
tasks.map(task => {
|
||||
const prio = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium
|
||||
@@ -470,7 +470,7 @@ export default function ProjectDetail() {
|
||||
|
||||
{/* ─── DISCUSSION SIDEBAR ─── */}
|
||||
{showDiscussion && (
|
||||
<div className="w-[340px] shrink-0 bg-white rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
|
||||
<div className="w-[340px] shrink-0 bg-surface rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-1.5">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
@@ -539,7 +539,7 @@ function TaskKanbanCard({ task, canEdit, canDelete, onClick, onDelete, onStatusC
|
||||
onDragStart={(e) => canEdit && onDragStart(e, task)}
|
||||
onDragEnd={onDragEnd}
|
||||
onClick={onClick}
|
||||
className={`bg-white rounded-lg border border-border p-3 group hover:border-brand-primary/30 hover:shadow-sm transition-all cursor-pointer ${canEdit ? 'active:cursor-grabbing' : ''}`}
|
||||
className={`bg-surface rounded-lg border border-border p-3 group hover:border-brand-primary/30 hover:shadow-sm transition-all cursor-pointer ${canEdit ? 'active:cursor-grabbing' : ''}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${priority.color} mt-1.5 shrink-0`} title={priority.label} />
|
||||
@@ -572,7 +572,7 @@ function TaskKanbanCard({ task, canEdit, canDelete, onClick, onDelete, onStatusC
|
||||
)}
|
||||
{canDelete && (
|
||||
<button onClick={(e) => { e.stopPropagation(); onDelete() }}
|
||||
className="text-[10px] text-red-400 hover:bg-red-50 px-2 py-0.5 rounded-full flex items-center gap-1 ml-auto">
|
||||
className="text-[10px] text-red-400 hover:bg-red-50 px-2 py-0.5 rounded-full flex items-center gap-1 ms-auto">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
@@ -614,7 +614,7 @@ function GanttView({ tasks, project, onEditTask, onTaskColorChange }) {
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-border py-16 text-center">
|
||||
<div className="bg-surface rounded-xl border border-border py-16 text-center">
|
||||
<GanttChart className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
|
||||
<p className="text-text-secondary font-medium">No tasks to display</p>
|
||||
<p className="text-sm text-text-tertiary mt-1">Add tasks with due dates to see the timeline</p>
|
||||
@@ -666,7 +666,7 @@ function GanttView({ tasks, project, onEditTask, onTaskColorChange }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
<div className="bg-surface rounded-xl border border-border overflow-hidden">
|
||||
{/* Zoom toolbar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-surface-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -757,7 +757,7 @@ function GanttView({ tasks, project, onEditTask, onTaskColorChange }) {
|
||||
)}
|
||||
{!onTaskColorChange && <div className={`w-2 h-2 rounded-full ${prio.color} shrink-0`} />}
|
||||
<button onClick={() => onEditTask(task)}
|
||||
className="text-xs font-medium text-text-primary truncate hover:text-brand-primary text-left">
|
||||
className="text-xs font-medium text-text-primary truncate hover:text-brand-primary text-start">
|
||||
{task.title}
|
||||
</button>
|
||||
</div>
|
||||
@@ -787,7 +787,7 @@ function GanttView({ tasks, project, onEditTask, onTaskColorChange }) {
|
||||
{colorPicker && onTaskColorChange && (
|
||||
<div
|
||||
ref={colorPickerRef}
|
||||
className="fixed z-50 bg-white rounded-lg shadow-xl border border-border p-2"
|
||||
className="fixed z-50 bg-surface rounded-lg shadow-xl border border-border p-2"
|
||||
style={{ left: colorPicker.x, top: colorPicker.y }}
|
||||
>
|
||||
<div className="grid grid-cols-4 gap-1.5 mb-2">
|
||||
|
||||
Reference in New Issue
Block a user