1	import { useState, useEffect } from 'react';
2	import { useNavigate } from 'react-router-dom';
3	import { TrendingUp, Clock, AlertCircle, CheckCircle2, ChevronRight, AlertTriangle } from 'lucide-react';
4	import api from '../api';
5	import { getPriorityClass, getCategoryClass, CATEGORIES, formatDate, isOverdue } from '../utils';
6
7	function MiniBar({ value, max, color = 'bg-primary' }) {
8	  const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
9	  return (
10

11	      <div className={`h-full ${color} rounded-full transition-all duration-700`} style={{ width: `${pct}%` }} />
12

13	  );
14	}
15
16	const CAT_COLORS = {
17	  ai: 'bg-blue-500', vendor: 'bg-purple-500', gov: 'bg-amber-500',
18	  people: 'bg-teal-500', events: 'bg-orange-500', strategic: 'bg-green-500', adhoc: 'bg-red-500',
19	};
20
21	export default function RightPanel() {
22	  const [stats, setStats] = useState(null);
23	  const [weekly, setWeekly] = useState(null);
24	  const [projects, setProjects] = useState([]);
25	  const [upcoming, setUpcoming] = useState([]);
26	  const navigate = useNavigate();
27
28	  useEffect(() => {
29	    Promise.all([
30	      api.get('/tasks/stats'),
31	      api.get('/weekly-review'),
32	      api.get('/projects'),
33	      api.get('/tasks'),
34	    ]).then(([s, w, pr, tasks]) => {
35	      setStats(s);
36	      setWeekly(w);
37	      // Top 4 active projects by category diversity
38	      const active = pr.filter(p => p.status !== 'Done').slice(0, 4);
39	      setProjects(active);
40	      // Upcoming tasks with due dates
41	      const now = new Date().toISOString().split('T')[0];
42	      const soon = tasks
43	        .filter(t => t.due_date && t.status !== 'Done')
44	        .sort((a, b) => a.due_date.localeCompare(b.due_date))
45	        .slice(0, 4);
46	      setUpcoming(soon);
47	    }).catch(() => {});
48	  }, []);
49
50	  // Category breakdown
51	  const catCounts = projects.reduce((acc, p) => {
52	    acc[p.category] = (acc[p.category] || 0) + 1;
53	    return acc;
54	  }, {});
55
56	  return (
57
58

59
60	        {/* Executive Summary */}
61

62
Executive Summary

63

64	            {[
65	              { label: 'Active Projects', value: stats?.activeProjects ?? '—', color: 'text-primary', bg: 'bg-primary/10' },
66	              { label: 'Open Tasks',      value: stats?.openTasks ?? '—',      color: 'text-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/20' },
67	              { label: 'Follow-ups',      value: stats?.openFollowUps ?? '—',  color: 'text-violet-600', bg: 'bg-violet-50 dark:bg-violet-900/20' },
68	              { label: 'Today\'s Focus',  value: stats?.todayFocus ?? '—',     color: 'text-teal-600', bg: 'bg-teal-50 dark:bg-teal-900/20' },
69	            ].map(({ label, value, color, bg }) => (
70	              <div key={label} className={`${bg} rounded-xl p-3`}>
71	                <p className={`text-xl font-bold ${color}`}>{value}

72
{label}

73

74	            ))}
75

76

77
78	        {/* This Week */}
79	        {weekly && (
80

81
This Week

82

83

84

85	                  <CheckCircle2 size={13} className="text-green-500" />
86	                  Completed
87

88	                {weekly.completedThisWeek.length}
89

90	              <MiniBar value={weekly.completedThisWeek.length} max={Math.max(weekly.completedThisWeek.length + weekly.addedThisWeek.length, 1)} color="bg-green-500" />
91

92

93	                  <TrendingUp size={13} className="text-blue-500" />
94	                  Added
95

96	                {weekly.addedThisWeek.length}
97

98

99

100	                  <AlertCircle size={13} className="text-red-500" />
101	                  Overdue
102

103	                {weekly.overdue.length}
104

105

106

107	        )}
108
109	        {/* Project health */}
110

111

112
Active Projects

113	            <button onClick={() => navigate('/projects')} className="text-[10px] text-primary hover:underline">View all</button>
114

115

116	            {projects.map(p => (
117	              <button
118	                key={p.id}
119	                onClick={() => navigate(`/projects/${p.id}`)}
120	                className="w-full text-left card p-3 hover:shadow-hover transition-all duration-200 hover:-translate-y-0.5"
121	              >
122

123	                  <div className={`w-2 h-2 rounded-full shrink-0 mt-1.5 ${CAT_COLORS[p.category]}`} />
124
{p.name}

125	                  <ChevronRight size={11} className="text-gray-300 shrink-0 mt-1" />
126

127

128	                  {p.priority}
132	                  {p.open_task_count} tasks
133

134	              </button>
135	            ))}
136

137

138
139	        {/* Upcoming due dates */}
140	        {upcoming.length > 0 && (
141

142
Upcoming Deadlines

143

144	              {upcoming.map(t => {
145	                const over = isOverdue(t.due_date);
146	                return (
147

148	                    <Clock size={12} className={`shrink-0 mt-0.5 ${over ? 'text-red-500' : 'text-gray-400'}`} />
149

150
{t.title}

151	                      <p className={`text-[10px] mt-0.5 ${over ? 'text-red-500 font-medium' : 'text-gray-400'}`}>
152	                        {over && <AlertTriangle size={9} className="text-red-500 shrink-0" />}
153	                        {over ? 'Overdue · ' : ''}{formatDate(t.due_date)}
154

155

156

157	                );
158	              })}
159

160

161	        )}
162

163
164	  );
165	}
166

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { TrendingUp, Clock, AlertCircle, CheckCircle2, ChevronRight, AlertTriangle } from 'lucide-react';
import api from '../api';
import { getPriorityClass, getCategoryClass, CATEGORIES, formatDate, isOverdue } from '../utils';

function MiniBar({ value, max, color = 'bg-primary' }) {
  const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
  return (

      <div className={`h-full ${color} rounded-full transition-all duration-700`} style={{ width: `${pct}%` }} />

  );
}

const CAT_COLORS = {
  ai: 'bg-blue-500', vendor: 'bg-purple-500', gov: 'bg-amber-500',
  people: 'bg-teal-500', events: 'bg-orange-500', strategic: 'bg-green-500', adhoc: 'bg-red-500',
};

export default function RightPanel() {
  const [stats, setStats] = useState(null);
  const [weekly, setWeekly] = useState(null);
  const [projects, setProjects] = useState([]);
  const [upcoming, setUpcoming] = useState([]);
  const navigate = useNavigate();

  useEffect(() => {
    Promise.all([
      api.get('/tasks/stats'),
      api.get('/weekly-review'),
      api.get('/projects'),
      api.get('/tasks'),
    ]).then(([s, w, pr, tasks]) => {
      setStats(s);
      setWeekly(w);
      // Top 4 active projects by category diversity
      const active = pr.filter(p => p.status !== 'Done').slice(0, 4);
      setProjects(active);
      // Upcoming tasks with due dates
      const now = new Date().toISOString().split('T')[0];
      const soon = tasks
        .filter(t => t.due_date && t.status !== 'Done')
        .sort((a, b) => a.due_date.localeCompare(b.due_date))
        .slice(0, 4);
      setUpcoming(soon);
    }).catch(() => {});
  }, []);

  // Category breakdown
  const catCounts = projects.reduce((acc, p) => {
    acc[p.category] = (acc[p.category] || 0) + 1;
    return acc;
  }, {});

  return (

        {/* Executive Summary */}

Executive Summary

            {[
              { label: 'Active Projects', value: stats?.activeProjects ?? '—', color: 'text-primary', bg: 'bg-primary/10' },
              { label: 'Open Tasks',      value: stats?.openTasks ?? '—',      color: 'text-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/20' },
              { label: 'Follow-ups',      value: stats?.openFollowUps ?? '—',  color: 'text-violet-600', bg: 'bg-violet-50 dark:bg-violet-900/20' },
              { label: 'Today\'s Focus',  value: stats?.todayFocus ?? '—',     color: 'text-teal-600', bg: 'bg-teal-50 dark:bg-teal-900/20' },
            ].map(({ label, value, color, bg }) => (
              <div key={label} className={`${bg} rounded-xl p-3`}>
                <p className={`text-xl font-bold ${color}`}>{value}

{label}

            ))}

        {/* This Week */}
        {weekly && (

This Week

                  <CheckCircle2 size={13} className="text-green-500" />
                  Completed

                {weekly.completedThisWeek.length}

              <MiniBar value={weekly.completedThisWeek.length} max={Math.max(weekly.completedThisWeek.length + weekly.addedThisWeek.length, 1)} color="bg-green-500" />

                  <TrendingUp size={13} className="text-blue-500" />
                  Added

                {weekly.addedThisWeek.length}

                  <AlertCircle size={13} className="text-red-500" />
                  Overdue

                {weekly.overdue.length}

        )}

        {/* Project health */}

Active Projects

            <button onClick={() => navigate('/projects')} className="text-[10px] text-primary hover:underline">View all</button>

            {projects.map(p => (
              <button
                key={p.id}
                onClick={() => navigate(`/projects/${p.id}`)}
                className="w-full text-left card p-3 hover:shadow-hover transition-all duration-200 hover:-translate-y-0.5"
              >

                  <div className={`w-2 h-2 rounded-full shrink-0 mt-1.5 ${CAT_COLORS[p.category]}`} />

{p.name}

                  <ChevronRight size={11} className="text-gray-300 shrink-0 mt-1" />

                  {p.priority}
                  {p.open_task_count} tasks

              </button>
            ))}

        {/* Upcoming due dates */}
        {upcoming.length > 0 && (

Upcoming Deadlines

              {upcoming.map(t => {
                const over = isOverdue(t.due_date);
                return (

                    <Clock size={12} className={`shrink-0 mt-0.5 ${over ? 'text-red-500' : 'text-gray-400'}`} />

{t.title}

                      <p className={`text-[10px] mt-0.5 ${over ? 'text-red-500 font-medium' : 'text-gray-400'}`}>
                        {over && <AlertTriangle size={9} className="text-red-500 shrink-0" />}
                        {over ? 'Overdue · ' : ''}{formatDate(t.due_date)}

                );
              })}

        )}

  );
}


1	import { useState, useEffect } from 'react';
2	import { Plus, FolderKanban, ChevronRight, CheckSquare, Pencil, Trash2, CheckCircle2, Clock, Loader2, PauseCircle } from 'lucide-react';
3	import { Link } from 'react-router-dom';
4	import toast from 'react-hot-toast';
5	import api from '../api';
6	import ConfirmDialog from './ConfirmDialog';
7	import { CATEGORIES, STATUSES, PRIORITIES, getCategoryClass, getPriorityClass, getStatusClass, formatDate } from '../utils';
8
9	const ALL_CATS = [{ id: '', label: 'All' }, ...Object.entries(CATEGORIES).map(([id, v]) => ({ id, label: v.label }))];
10	const ALL_STATUSES = ['All', ...STATUSES.filter(s => s !== 'Done'), 'Done'];
11
12	const CATEGORY_ACCENT = {
13	  ai:        'from-blue-500 to-cyan-500',
14	  vendor:    'from-purple-500 to-violet-500',
15	  gov:       'from-amber-500 to-yellow-500',
16	  people:    'from-teal-500 to-emerald-500',
17	  events:    'from-orange-500 to-red-400',
18	  strategic: 'from-green-500 to-teal-500',
19	  adhoc:     'from-red-500 to-rose-500',
20	};
21
22	const CATEGORY_ICON_BG = {
23	  ai:        'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
24	  vendor:    'bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
25	  gov:       'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400',
26	  people:    'bg-teal-50 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400',
27	  events:    'bg-orange-50 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400',
28	  strategic: 'bg-green-50 dark:bg-green-900/30 text-green-600 dark:text-green-400',
29	  adhoc:     'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400',
30	};
31
32	const STATUS_ICON = {
33	  Open:         { icon: Clock,        cls: 'text-gray-400'  },
34	  'In Progress':{ icon: Loader2,      cls: 'text-blue-500'  },
35	  Waiting:      { icon: PauseCircle,  cls: 'text-amber-500' },
36	  Done:         { icon: CheckCircle2, cls: 'text-green-500' },
37	};
38
39	function ProjectModal({ project, onSave, onClose }) {
40	  const [form, setForm] = useState({
41	    name: '', category: 'ai', status: 'Open', priority: 'Medium',
42	    description: '', last_update: '', next_step: '',
43	    ...project,
44	  });
45	  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
46	  const submit = async (e) => {
47	    e.preventDefault();
48	    onSave(form);
49	  };
50	  return (
51

52

53
 e.stopPropagation()}>
54

55
{project?.id ? 'Edit Project' : 'New Project'}

56

57	        <form onSubmit={submit} className="px-5 py-4 space-y-3">
58

59	            Project Name *
60	            <input className="input" required value={form.name} onChange={e => set('name', e.target.value)} autoFocus />
61

62

63

64	              Category
65	              <select className="select" value={form.category} onChange={e => set('category', e.target.value)}>
66	                {Object.entries(CATEGORIES).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
67	              </select>
68

69

70	              Priority
71	              <select className="select" value={form.priority} onChange={e => set('priority', e.target.value)}>
72	                {PRIORITIES.map(p => <option key={p}>{p}</option>)}
73	              </select>
74

75

76

77	            Status
78	            <select className="select" value={form.status} onChange={e => set('status', e.target.value)}>
79	              {STATUSES.map(s => <option key={s}>{s}</option>)}
80	            </select>
81

82

83	            Description
84	            <textarea className="input resize-none" rows={2} value={form.description} onChange={e => set('description', e.target.value)} />
85

86

87	            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
88	            <button type="submit" className="btn-primary">{project?.id ? 'Save' : 'Create Project'}</button>
89

90	        </form>
91

92

93	  );
94	}
95
96	export default function Projects() {
97	  const [projects, setProjects] = useState([]);
98	  const [loading, setLoading] = useState(true);
99	  const [catFilter, setCatFilter] = useState('');
100	  const [statusFilter, setStatusFilter] = useState('All');
101	  const [editProject, setEditProject] = useState(null);
102	  const [deleteProject, setDeleteProject] = useState(null);
103	  const [showNew, setShowNew] = useState(false);
104
105	  const load = async () => {
106	    setLoading(true);
107	    const params = new URLSearchParams();
108	    if (catFilter) params.set('category', catFilter);
109	    if (statusFilter !== 'All') params.set('status', statusFilter);
110	    try {
111	      setProjects(await api.get('/projects?' + params));
112	    } finally { setLoading(false); }
113	  };
114
115	  useEffect(() => { load(); }, [catFilter, statusFilter]);
116
117	  const handleSave = async (form) => {
118	    try {
119	      if (form.id) { await api.put(`/projects/${form.id}`, form); toast.success('Project updated'); }
120	      else { await api.post('/projects', form); toast.success('Project created'); }
121	      setEditProject(null); setShowNew(false); load();
122	    } catch (e) { toast.error(e.message); }
123	  };
124
125	  const handleDelete = async () => {
126	    try {
127	      await api.delete(`/projects/${deleteProject.id}`);
128	      toast.success('Project deleted');
129	      setDeleteProject(null);
130	      load();
131	    } catch (e) { toast.error(e.message); }
132	  };
133
134	  return (
135

136	      {/* Header */}
137

138

139

140
Projects

141	            {!loading &&
{projects.length} project{projects.length !== 1 ? 's' : ''}
}
142

143	          <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5 py-2">
144	            <Plus size={14} /> New Project
145	          </button>
146

147
148	        {/* Category pills */}
149

150	          {ALL_CATS.map(c => (
151	            <button
152	              key={c.id}
153	              onClick={() => setCatFilter(c.id)}
154	              className={`shrink-0 px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
155	                catFilter === c.id
156	                  ? 'bg-primary text-white'
157	                  : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
158	              }`}
159	            >
160	              {c.label}
161	            </button>
162	          ))}
163

164
165	        {/* Status tabs */}
166

167	          {ALL_STATUSES.map(s => (
168	            <button
169	              key={s}
170	              onClick={() => setStatusFilter(s)}
171	              className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
172	                statusFilter === s
173	                  ? 'bg-gray-800 dark:bg-white text-white dark:text-gray-900'
174	                  : 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
175	              }`}
176	            >
177	              {s}
178	            </button>
179	          ))}
180

181

182
183	      {/* Grid */}
184

185	        {loading ? (
186

187	            {[...Array(9)].map((_, i) =>
)}
188

189	        ) : projects.length === 0 ? (
190

191	            <FolderKanban size={44} className="text-gray-200 dark:text-gray-700" />
192
No projects found

193	            <button onClick={() => setShowNew(true)} className="btn-primary">Create a project</button>
194

195	        ) : (
196

197	            {projects.map(p => {
198	              const StatusIcon = STATUS_ICON[p.status]?.icon || Clock;
199	              const statusCls  = STATUS_ICON[p.status]?.cls  || 'text-gray-400';
200	              const isDone     = p.status === 'Done';
201	              return (
202	                <Link
203	                  to={`/projects/${p.id}`}
204	                  key={p.id}
205	                  className={`group relative bg-white dark:bg-gray-900 rounded-2xl border border-gray-100 dark:border-gray-800 shadow-card hover:shadow-hover hover:-translate-y-1 transition-all duration-200 overflow-hidden flex flex-col ${isDone ? 'opacity-70' : ''}`}
206	                >
207	                  {/* Colored top accent bar */}
208	                  <div className={`h-1.5 w-full bg-gradient-to-r ${CATEGORY_ACCENT[p.category] || 'from-gray-400 to-gray-500'}`} />
209
210

211	                    {/* Icon + category + arrow */}
212

213	                      <div className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${CATEGORY_ICON_BG[p.category] || 'bg-gray-100 text-gray-500'}`}>
214	                        <FolderKanban size={16} />
215

216

217	                        <span className={`badge text-[10px] py-0.5 ${getCategoryClass(p.category)}`}>
218	                          {CATEGORIES[p.category]?.label?.split(' ')[0] || p.category}
219
220

221	                      <ChevronRight size={13} className="text-gray-300 dark:text-gray-600 shrink-0 mt-1 group-hover:text-primary group-hover:translate-x-0.5 transition-all duration-150" />
222

223
224	                    {/* Title */}
225

226	                      {p.name}
227

228
229	                    {/* Description */}
230	                    {p.description ? (
231

232	                        {p.description}
233

234	                    ) :
}
235
236	                    {/* Priority */}
237

238	                      <span className={`badge text-[10px] py-0.5 ${getPriorityClass(p.priority)}`}>{p.priority}
239

240
241	                    {/* Footer */}
242

243	                      {/* Status with icon */}
244

245	                        <StatusIcon size={11} className={statusCls} />
246	                        {p.status}
247

248	                      {/* Task count */}
249

250	                        <CheckSquare size={10} className="text-gray-400" />
251	                        {p.open_task_count || 0}
252

253

254

255
256	                  {/* Edit / Delete actions on hover */}
257

258	                    <button
259	                      onClick={e => { e.preventDefault(); setEditProject(p); }}
260	                      className="w-6 h-6 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-400 hover:text-primary hover:border-primary shadow-sm transition-all"
261	                    >
262	                      <Pencil size={10} />
263	                    </button>
264	                    <button
265	                      onClick={e => { e.preventDefault(); setDeleteProject(p); }}
266	                      className="w-6 h-6 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-400 hover:text-red-500 hover:border-red-300 shadow-sm transition-all"
267	                    >
268	                      <Trash2 size={10} />
269	                    </button>
270

271	                </Link>
272	              );
273	            })}
274

275	        )}
276

277
278	      {(showNew || editProject) && (
279	        <ProjectModal
280	          project={editProject}
281	          onSave={handleSave}
282	          onClose={() => { setShowNew(false); setEditProject(null); }}
283	        />
284	      )}
285	      {deleteProject && (
286	        <ConfirmDialog
287	          title="Delete project?"
288	          message={`"${deleteProject.name}" and its associations will be removed.`}
289	          onConfirm={handleDelete}
290	          onCancel={() => setDeleteProject(null)}
291	        />
292	      )}
293

294	  );
295	}
296

import { useState, useEffect } from 'react';
import { Plus, FolderKanban, ChevronRight, CheckSquare, Pencil, Trash2, CheckCircle2, Clock, Loader2, PauseCircle } from 'lucide-react';
import { Link } from 'react-router-dom';
import toast from 'react-hot-toast';
import api from '../api';
import ConfirmDialog from './ConfirmDialog';
import { CATEGORIES, STATUSES, PRIORITIES, getCategoryClass, getPriorityClass, getStatusClass, formatDate } from '../utils';

const ALL_CATS = [{ id: '', label: 'All' }, ...Object.entries(CATEGORIES).map(([id, v]) => ({ id, label: v.label }))];
const ALL_STATUSES = ['All', ...STATUSES.filter(s => s !== 'Done'), 'Done'];

const CATEGORY_ACCENT = {
  ai:        'from-blue-500 to-cyan-500',
  vendor:    'from-purple-500 to-violet-500',
  gov:       'from-amber-500 to-yellow-500',
  people:    'from-teal-500 to-emerald-500',
  events:    'from-orange-500 to-red-400',
  strategic: 'from-green-500 to-teal-500',
  adhoc:     'from-red-500 to-rose-500',
};

const CATEGORY_ICON_BG = {
  ai:        'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
  vendor:    'bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
  gov:       'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400',
  people:    'bg-teal-50 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400',
  events:    'bg-orange-50 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400',
  strategic: 'bg-green-50 dark:bg-green-900/30 text-green-600 dark:text-green-400',
  adhoc:     'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400',
};

const STATUS_ICON = {
  Open:         { icon: Clock,        cls: 'text-gray-400'  },
  'In Progress':{ icon: Loader2,      cls: 'text-blue-500'  },
  Waiting:      { icon: PauseCircle,  cls: 'text-amber-500' },
  Done:         { icon: CheckCircle2, cls: 'text-green-500' },
};

function ProjectModal({ project, onSave, onClose }) {
  const [form, setForm] = useState({
    name: '', category: 'ai', status: 'Open', priority: 'Medium',
    description: '', last_update: '', next_step: '',
    ...project,
  });
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  const submit = async (e) => {
    e.preventDefault();
    onSave(form);
  };
  return (

 e.stopPropagation()}>

{project?.id ? 'Edit Project' : 'New Project'}

        <form onSubmit={submit} className="px-5 py-4 space-y-3">

            Project Name *
            <input className="input" required value={form.name} onChange={e => set('name', e.target.value)} autoFocus />

              Category
              <select className="select" value={form.category} onChange={e => set('category', e.target.value)}>
                {Object.entries(CATEGORIES).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
              </select>

              Priority
              <select className="select" value={form.priority} onChange={e => set('priority', e.target.value)}>
                {PRIORITIES.map(p => <option key={p}>{p}</option>)}
              </select>

            Status
            <select className="select" value={form.status} onChange={e => set('status', e.target.value)}>
              {STATUSES.map(s => <option key={s}>{s}</option>)}
            </select>

            Description
            <textarea className="input resize-none" rows={2} value={form.description} onChange={e => set('description', e.target.value)} />

            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
            <button type="submit" className="btn-primary">{project?.id ? 'Save' : 'Create Project'}</button>

        </form>

  );
}

export default function Projects() {
  const [projects, setProjects] = useState([]);
  const [loading, setLoading] = useState(true);
  const [catFilter, setCatFilter] = useState('');
  const [statusFilter, setStatusFilter] = useState('All');
  const [editProject, setEditProject] = useState(null);
  const [deleteProject, setDeleteProject] = useState(null);
  const [showNew, setShowNew] = useState(false);

  const load = async () => {
    setLoading(true);
    const params = new URLSearchParams();
    if (catFilter) params.set('category', catFilter);
    if (statusFilter !== 'All') params.set('status', statusFilter);
    try {
      setProjects(await api.get('/projects?' + params));
    } finally { setLoading(false); }
  };

  useEffect(() => { load(); }, [catFilter, statusFilter]);

  const handleSave = async (form) => {
    try {
      if (form.id) { await api.put(`/projects/${form.id}`, form); toast.success('Project updated'); }
      else { await api.post('/projects', form); toast.success('Project created'); }
      setEditProject(null); setShowNew(false); load();
    } catch (e) { toast.error(e.message); }
  };

  const handleDelete = async () => {
    try {
      await api.delete(`/projects/${deleteProject.id}`);
      toast.success('Project deleted');
      setDeleteProject(null);
      load();
    } catch (e) { toast.error(e.message); }
  };

  return (

      {/* Header */}

Projects

            {!loading &&
{projects.length} project{projects.length !== 1 ? 's' : ''}
}

          <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5 py-2">
            <Plus size={14} /> New Project
          </button>

        {/* Category pills */}

          {ALL_CATS.map(c => (
            <button
              key={c.id}
              onClick={() => setCatFilter(c.id)}
              className={`shrink-0 px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
                catFilter === c.id
                  ? 'bg-primary text-white'
                  : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
              }`}
            >
              {c.label}
            </button>
          ))}

        {/* Status tabs */}

          {ALL_STATUSES.map(s => (
            <button
              key={s}
              onClick={() => setStatusFilter(s)}
              className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
                statusFilter === s
                  ? 'bg-gray-800 dark:bg-white text-white dark:text-gray-900'
                  : 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
              }`}
            >
              {s}
            </button>
          ))}

      {/* Grid */}

        {loading ? (

            {[...Array(9)].map((_, i) =>
)}

        ) : projects.length === 0 ? (

            <FolderKanban size={44} className="text-gray-200 dark:text-gray-700" />

No projects found

            <button onClick={() => setShowNew(true)} className="btn-primary">Create a project</button>

        ) : (

            {projects.map(p => {
              const StatusIcon = STATUS_ICON[p.status]?.icon || Clock;
              const statusCls  = STATUS_ICON[p.status]?.cls  || 'text-gray-400';
              const isDone     = p.status === 'Done';
              return (
                <Link
                  to={`/projects/${p.id}`}
                  key={p.id}
                  className={`group relative bg-white dark:bg-gray-900 rounded-2xl border border-gray-100 dark:border-gray-800 shadow-card hover:shadow-hover hover:-translate-y-1 transition-all duration-200 overflow-hidden flex flex-col ${isDone ? 'opacity-70' : ''}`}
                >
                  {/* Colored top accent bar */}
                  <div className={`h-1.5 w-full bg-gradient-to-r ${CATEGORY_ACCENT[p.category] || 'from-gray-400 to-gray-500'}`} />

                    {/* Icon + category + arrow */}

                      <div className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${CATEGORY_ICON_BG[p.category] || 'bg-gray-100 text-gray-500'}`}>
                        <FolderKanban size={16} />

                        <span className={`badge text-[10px] py-0.5 ${getCategoryClass(p.category)}`}>
                          {CATEGORIES[p.category]?.label?.split(' ')[0] || p.category}

                      <ChevronRight size={13} className="text-gray-300 dark:text-gray-600 shrink-0 mt-1 group-hover:text-primary group-hover:translate-x-0.5 transition-all duration-150" />

                    {/* Title */}

                      {p.name}

                    {/* Description */}
                    {p.description ? (

                        {p.description}

                    ) :
}

                    {/* Priority */}

                      <span className={`badge text-[10px] py-0.5 ${getPriorityClass(p.priority)}`}>{p.priority}

                    {/* Footer */}

                      {/* Status with icon */}

                        <StatusIcon size={11} className={statusCls} />
                        {p.status}

                      {/* Task count */}

                        <CheckSquare size={10} className="text-gray-400" />
                        {p.open_task_count || 0}

                  {/* Edit / Delete actions on hover */}

                    <button
                      onClick={e => { e.preventDefault(); setEditProject(p); }}
                      className="w-6 h-6 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-400 hover:text-primary hover:border-primary shadow-sm transition-all"
                    >
                      <Pencil size={10} />
                    </button>
                    <button
                      onClick={e => { e.preventDefault(); setDeleteProject(p); }}
                      className="w-6 h-6 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-400 hover:text-red-500 hover:border-red-300 shadow-sm transition-all"
                    >
                      <Trash2 size={10} />
                    </button>

                </Link>
              );
            })}

        )}

      {(showNew || editProject) && (
        <ProjectModal
          project={editProject}
          onSave={handleSave}
          onClose={() => { setShowNew(false); setEditProject(null); }}
        />
      )}
      {deleteProject && (
        <ConfirmDialog
          title="Delete project?"
          message={`"${deleteProject.name}" and its associations will be removed.`}
          onConfirm={handleDelete}
          onCancel={() => setDeleteProject(null)}
        />
      )}

  );
}


1	import { useState, useEffect } from 'react';
2	import { Users, Plus, User, X, Pencil, Trash2, Search, ChevronUp, ChevronDown, Phone, MapPin, Building2, Layers, GitBranch, BadgeCheck } from 'lucide-react';
3	import toast from 'react-hot-toast';
4	import api from '../api';
5	import ConfirmDialog from './ConfirmDialog';
6
7	const FIELDS = [
8	  { key: 'pr_number',     label: 'PR No.',        icon: BadgeCheck,  placeholder: 'e.g. 29184'         },
9	  { key: 'grade',         label: 'Grade',         icon: Layers,      placeholder: 'e.g. G8'            },
10	  { key: 'designation',   label: 'Designation',   icon: BadgeCheck,  placeholder: 'e.g. Mgr - Digital' },
11	  { key: 'department',    label: 'Department',    icon: Building2,   placeholder: 'e.g. Digital'       },
12	  { key: 'section',       label: 'Section',       icon: Layers,      placeholder: 'e.g. AI Strategy'  },
13	  { key: 'reporting_line',label: 'Reporting Line',icon: GitBranch,   placeholder: 'Reports to…'        },
14	  { key: 'mobile_number', label: 'Mobile',        icon: Phone,       placeholder: '+971 50 …'          },
15	  { key: 'place',         label: 'Place',         icon: MapPin,      placeholder: 'e.g. HQ'            },
16	];
17
18	const EMPTY_FORM = { name: '', pr_number: '', grade: '', designation: '', department: '', section: '', reporting_line: '', mobile_number: '', place: '' };
19
20	function Avatar({ name, size = 'md' }) {
21	  const s = size === 'lg' ? 'w-11 h-11 text-sm' : size === 'sm' ? 'w-7 h-7 text-[10px]' : 'w-9 h-9 text-xs';
22	  const initials = name && name !== 'Unassigned'
23	    ? name.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase()
24	    : null;
25	  return (
26	    <div className={`${s} rounded-full bg-gradient-to-br from-primary to-brand-pink flex items-center justify-center text-white font-bold shrink-0`}>
27	      {initials || <User size={14} />}
28

29	  );
30	}
31
32	function PersonModal({ person, onSave, onClose }) {
33	  const [form, setForm] = useState({ ...EMPTY_FORM, ...person });
34	  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
35
36	  return (
37

38

39
 e.stopPropagation()}>
40

41	          <Avatar name={form.name || '?'} size="md" />
42

43
{person?.id ? 'Edit Member' : 'Add Member'}

44	            {person?.id &&
{person.name}
}
45

46	          <button onClick={onClose} className="btn-icon"><X size={16} /></button>
47

48	        <form onSubmit={e => { e.preventDefault(); onSave(form); }} className="px-5 py-4 space-y-3">
49

50	            Full Name *
51	            <input className="input font-medium" required placeholder="Full name" value={form.name} onChange={e => set('name', e.target.value)} autoFocus />
52

53

54	            {FIELDS.map(({ key, label, placeholder }) => (
55

56	                {label}
57	                <input className="input text-sm" placeholder={placeholder} value={form[key] || ''} onChange={e => set(key, e.target.value)} />
58

59	            ))}
60

61

62	            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
63	            <button type="submit" className="btn-primary">{person?.id ? 'Save' : 'Add Member'}</button>
64

65	        </form>
66

67

68	  );
69	}
70
71	export default function People() {
72	  const [people, setPeople]         = useState([]);
73	  const [loading, setLoading]       = useState(true);
74	  const [search, setSearch]         = useState('');
75	  const [editPerson, setEditPerson] = useState(null);
76	  const [deletePerson, setDelPerson]= useState(null);
77	  const [showNew, setShowNew]       = useState(false);
78	  const [sortKey, setSortKey]       = useState('name');
79	  const [sortDir, setSortDir]       = useState('asc');
80
81	  const load = () => {
82	    setLoading(true);
83	    api.get('/people').then(setPeople).finally(() => setLoading(false));
84	  };
85	  useEffect(() => { load(); }, []);
86
87	  const handleSave = async (form) => {
88	    try {
89	      if (form.id) { await api.put(`/people/${form.id}`, form); toast.success('Updated'); }
90	      else         { await api.post('/people', form);            toast.success('Member added'); }
91	      setEditPerson(null); setShowNew(false); load();
92	    } catch (e) { toast.error(e.message); }
93	  };
94
95	  const handleDelete = async () => {
96	    await api.delete(`/people/${deletePerson.id}`);
97	    toast.success('Removed'); setDelPerson(null); load();
98	  };
99
100	  const toggleSort = (key) => {
101	    if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
102	    else { setSortKey(key); setSortDir('asc'); }
103	  };
104
105	  const filtered = people
106	    .filter(p => !search || p.name.toLowerCase().includes(search.toLowerCase()) ||
107	      (p.designation || '').toLowerCase().includes(search.toLowerCase()) ||
108	      (p.department || '').toLowerCase().includes(search.toLowerCase()) ||
109	      (p.pr_number || '').includes(search))
110	    .sort((a, b) => {
111	      const av = (a[sortKey] || '').toLowerCase();
112	      const bv = (b[sortKey] || '').toLowerCase();
113	      return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
114	    });
115
116	  const SortIcon = ({ col }) => {
117	    if (sortKey !== col) return <ChevronUp size={10} className="text-gray-300 dark:text-gray-600" />;
118	    return sortDir === 'asc'
119	      ? <ChevronUp size={10} className="text-primary" />
120	      : <ChevronDown size={10} className="text-primary" />;
121	  };
122
123	  const TH = ({ col, label }) => (
124	     toggleSort(col)}
127	    >
128
{label}<SortIcon col={col} />

129
130	  );
131
132	  return (
133

134	      {/* Header */}
135

136

137

138
My Team

139	            {!loading &&
{people.length} members
}
140

141	          <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5 py-2 text-sm">
142	            <Plus size={13} /> Add Member
143	          </button>
144

145

146	          <Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
147	          <input className="input pl-8 py-2 text-sm w-full" placeholder="Search by name, designation, department, PR…" value={search} onChange={e => setSearch(e.target.value)} />
148

149

150
151	      {/* Table */}
152

153	        {loading ? (
154

155	            {[...Array(8)].map((_, i) =>
)}
156

157	        ) : filtered.length === 0 ? (
158

159	            <Users size={44} className="text-gray-200 dark:text-gray-700" />
160
{search ? 'No members found' : 'No team members yet'}

161

162	        ) : (
163

164

165

166	                #
167
168
169
170
171
172
173
174
175
176
177

178

179

180	              {filtered.map((p, i) => (
181

182	                  {i + 1}
183
184

185	                      <Avatar name={p.name} size="sm" />
186	                      {p.name}
187

188
189
190	                    {p.pr_number
191	                      ? {p.pr_number}
192	                      : —}
193
194	                  {p.grade || —}
195
196	                    {p.designation || —}
197
198	                  {p.department || —}
199	                  {p.section || —}
200	                  {p.reporting_line || —}
201
202	                    {p.mobile_number
203	                      ? <a href={`tel:${p.mobile_number}`} className="text-primary hover:underline" onClick={e => e.stopPropagation()}>{p.mobile_number}
204	                      : —}
205
206	                  {p.place || —}
207
208

209	                      <button onClick={() => setEditPerson(p)} className="w-6 h-6 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-400 hover:text-primary hover:bg-primary/10 transition-colors">
210	                        <Pencil size={11} />
211	                      </button>
212	                      <button onClick={() => setDelPerson(p)} className="w-6 h-6 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
213	                        <Trash2 size={11} />
214	                      </button>
215

216
217

218	              ))}
219

220

221	        )}
222

223
224	      {(showNew || editPerson) && (
225	        <PersonModal person={editPerson} onSave={handleSave} onClose={() => { setShowNew(false); setEditPerson(null); }} />
226	      )}
227	      {deletePerson && (
228	        <ConfirmDialog title="Remove member?" message={`Remove "${deletePerson.name}" from your team?`} onConfirm={handleDelete} onCancel={() => setDelPerson(null)} />
229	      )}
230

231	  );
232	}
233

import { useState, useEffect } from 'react';
import { Users, Plus, User, X, Pencil, Trash2, Search, ChevronUp, ChevronDown, Phone, MapPin, Building2, Layers, GitBranch, BadgeCheck } from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../api';
import ConfirmDialog from './ConfirmDialog';

const FIELDS = [
  { key: 'pr_number',     label: 'PR No.',        icon: BadgeCheck,  placeholder: 'e.g. 29184'         },
  { key: 'grade',         label: 'Grade',         icon: Layers,      placeholder: 'e.g. G8'            },
  { key: 'designation',   label: 'Designation',   icon: BadgeCheck,  placeholder: 'e.g. Mgr - Digital' },
  { key: 'department',    label: 'Department',    icon: Building2,   placeholder: 'e.g. Digital'       },
  { key: 'section',       label: 'Section',       icon: Layers,      placeholder: 'e.g. AI Strategy'  },
  { key: 'reporting_line',label: 'Reporting Line',icon: GitBranch,   placeholder: 'Reports to…'        },
  { key: 'mobile_number', label: 'Mobile',        icon: Phone,       placeholder: '+971 50 …'          },
  { key: 'place',         label: 'Place',         icon: MapPin,      placeholder: 'e.g. HQ'            },
];

const EMPTY_FORM = { name: '', pr_number: '', grade: '', designation: '', department: '', section: '', reporting_line: '', mobile_number: '', place: '' };

function Avatar({ name, size = 'md' }) {
  const s = size === 'lg' ? 'w-11 h-11 text-sm' : size === 'sm' ? 'w-7 h-7 text-[10px]' : 'w-9 h-9 text-xs';
  const initials = name && name !== 'Unassigned'
    ? name.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase()
    : null;
  return (
    <div className={`${s} rounded-full bg-gradient-to-br from-primary to-brand-pink flex items-center justify-center text-white font-bold shrink-0`}>
      {initials || <User size={14} />}

  );
}

function PersonModal({ person, onSave, onClose }) {
  const [form, setForm] = useState({ ...EMPTY_FORM, ...person });
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  return (

 e.stopPropagation()}>

          <Avatar name={form.name || '?'} size="md" />

{person?.id ? 'Edit Member' : 'Add Member'}

            {person?.id &&
{person.name}
}

          <button onClick={onClose} className="btn-icon"><X size={16} /></button>

        <form onSubmit={e => { e.preventDefault(); onSave(form); }} className="px-5 py-4 space-y-3">

            Full Name *
            <input className="input font-medium" required placeholder="Full name" value={form.name} onChange={e => set('name', e.target.value)} autoFocus />

            {FIELDS.map(({ key, label, placeholder }) => (

                {label}
                <input className="input text-sm" placeholder={placeholder} value={form[key] || ''} onChange={e => set(key, e.target.value)} />

            ))}

            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
            <button type="submit" className="btn-primary">{person?.id ? 'Save' : 'Add Member'}</button>

        </form>

  );
}

export default function People() {
  const [people, setPeople]         = useState([]);
  const [loading, setLoading]       = useState(true);
  const [search, setSearch]         = useState('');
  const [editPerson, setEditPerson] = useState(null);
  const [deletePerson, setDelPerson]= useState(null);
  const [showNew, setShowNew]       = useState(false);
  const [sortKey, setSortKey]       = useState('name');
  const [sortDir, setSortDir]       = useState('asc');

  const load = () => {
    setLoading(true);
    api.get('/people').then(setPeople).finally(() => setLoading(false));
  };
  useEffect(() => { load(); }, []);

  const handleSave = async (form) => {
    try {
      if (form.id) { await api.put(`/people/${form.id}`, form); toast.success('Updated'); }
      else         { await api.post('/people', form);            toast.success('Member added'); }
      setEditPerson(null); setShowNew(false); load();
    } catch (e) { toast.error(e.message); }
  };

  const handleDelete = async () => {
    await api.delete(`/people/${deletePerson.id}`);
    toast.success('Removed'); setDelPerson(null); load();
  };

  const toggleSort = (key) => {
    if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortKey(key); setSortDir('asc'); }
  };

  const filtered = people
    .filter(p => !search || p.name.toLowerCase().includes(search.toLowerCase()) ||
      (p.designation || '').toLowerCase().includes(search.toLowerCase()) ||
      (p.department || '').toLowerCase().includes(search.toLowerCase()) ||
      (p.pr_number || '').includes(search))
    .sort((a, b) => {
      const av = (a[sortKey] || '').toLowerCase();
      const bv = (b[sortKey] || '').toLowerCase();
      return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
    });

  const SortIcon = ({ col }) => {
    if (sortKey !== col) return <ChevronUp size={10} className="text-gray-300 dark:text-gray-600" />;
    return sortDir === 'asc'
      ? <ChevronUp size={10} className="text-primary" />
      : <ChevronDown size={10} className="text-primary" />;
  };

  const TH = ({ col, label }) => (
     toggleSort(col)}
    >

{label}<SortIcon col={col} />

  );

  return (

      {/* Header */}

My Team

            {!loading &&
{people.length} members
}

          <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5 py-2 text-sm">
            <Plus size={13} /> Add Member
          </button>

          <Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
          <input className="input pl-8 py-2 text-sm w-full" placeholder="Search by name, designation, department, PR…" value={search} onChange={e => setSearch(e.target.value)} />

      {/* Table */}

        {loading ? (

            {[...Array(8)].map((_, i) =>
)}

        ) : filtered.length === 0 ? (

            <Users size={44} className="text-gray-200 dark:text-gray-700" />

{search ? 'No members found' : 'No team members yet'}

        ) : (

                #

              {filtered.map((p, i) => (

                  {i + 1}

                      <Avatar name={p.name} size="sm" />
                      {p.name}

                    {p.pr_number
                      ? {p.pr_number}
                      : —}

                  {p.grade || —}

                    {p.designation || —}

                  {p.department || —}
                  {p.section || —}
                  {p.reporting_line || —}

                    {p.mobile_number
                      ? <a href={`tel:${p.mobile_number}`} className="text-primary hover:underline" onClick={e => e.stopPropagation()}>{p.mobile_number}
                      : —}

                  {p.place || —}

                      <button onClick={() => setEditPerson(p)} className="w-6 h-6 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-400 hover:text-primary hover:bg-primary/10 transition-colors">
                        <Pencil size={11} />
                      </button>
                      <button onClick={() => setDelPerson(p)} className="w-6 h-6 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
                        <Trash2 size={11} />
                      </button>

              ))}

        )}

      {(showNew || editPerson) && (
        <PersonModal person={editPerson} onSave={handleSave} onClose={() => { setShowNew(false); setEditPerson(null); }} />
      )}
      {deletePerson && (
        <ConfirmDialog title="Remove member?" message={`Remove "${deletePerson.name}" from your team?`} onConfirm={handleDelete} onCancel={() => setDelPerson(null)} />
      )}

  );
}


Let me check the Vite config and remaining components for issues.


client/src/api.js
client/src/utils.js
client/postcss.config.js
client/tailwind.config.js
client/vite.config.js


1	import { useState, useEffect } from 'react';
2	import { Plus, FileText, Trash2, Pencil, Calendar, FolderKanban } from 'lucide-react';
3	import toast from 'react-hot-toast';
4	import api from '../api';
5	import ConfirmDialog from './ConfirmDialog';
6	import { formatDate } from '../utils';
7
8	const NOTE_COLORS = [
9	  'bg-violet-50 dark:bg-violet-900/20 border-violet-200 dark:border-violet-800',
10	  'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
11	  'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800',
12	  'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800',
13	  'bg-pink-50 dark:bg-pink-900/20 border-pink-200 dark:border-pink-800',
14	  'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
15	];
16
17	function NoteModal({ note, projects, onSave, onClose }) {
18	  const [form, setForm] = useState({
19	    title: '', body: '', project_id: null,
20	    date: new Date().toISOString().split('T')[0], ...note,
21	  });
22	  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
23	  return (
24

25

26
 e.stopPropagation()}>
27

28
{note?.id ? 'Edit Note' : 'New Note'}

29

30	        <form onSubmit={e => { e.preventDefault(); onSave(form); }} className="px-5 py-4 space-y-3">
31	          <input className="input font-medium" required placeholder="Title" value={form.title} onChange={e => set('title', e.target.value)} autoFocus />
32	          <textarea className="input resize-none" rows={7} placeholder="Write your note…" value={form.body} onChange={e => set('body', e.target.value)} />
33

34

35	              Date
36	              <input type="date" className="input" value={form.date} onChange={e => set('date', e.target.value)} />
37

38

39	              Project
40	              <select className="select" value={form.project_id || ''} onChange={e => set('project_id', e.target.value || null)}>
41	                <option value="">No project</option>
42	                {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
43	              </select>
44

45

46

47	            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
48	            <button type="submit" className="btn-primary">{note?.id ? 'Save' : 'Add Note'}</button>
49

50	        </form>
51

52

53	  );
54	}
55
56	function NoteCard({ note, colorClass, onEdit, onDelete }) {
57	  const [expanded, setExpanded] = useState(false);
58	  return (
59	    <div className={`rounded-2xl border p-4 flex flex-col gap-2 group transition-all duration-200 hover:shadow-hover hover:-translate-y-0.5 ${colorClass}`}>
60

61
{note.title}

62

63	          <button onClick={() => onEdit(note)} className="p-1.5 rounded-lg hover:bg-white/60 dark:hover:bg-gray-700 text-gray-500 transition-colors">
64	            <Pencil size={12} />
65	          </button>
66	          <button onClick={() => onDelete(note)} className="p-1.5 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 transition-colors">
67	            <Trash2 size={12} />
68	          </button>
69

70

71
72	      {note.body && (
73	        <p className={`text-xs text-gray-600 dark:text-gray-400 leading-relaxed whitespace-pre-wrap ${!expanded ? 'line-clamp-4' : ''}`}>
74	          {note.body}
75

76	      )}
77	      {note.body && note.body.length > 200 && (
78	        <button onClick={() => setExpanded(!expanded)} className="text-[10px] font-medium text-primary hover:underline self-start">
79	          {expanded ? 'Show less' : 'Read more'}
80	        </button>
81	      )}
82
83

84	        <Calendar size={10} className="text-gray-400 shrink-0" />
85	        {formatDate(note.date)}
86	        {note.project_name && (
87
88	            <FolderKanban size={9} />{note.project_name}
89
90	        )}
91

92

93	  );
94	}
95
96	export default function Notes() {
97	  const [notes, setNotes]       = useState([]);
98	  const [projects, setProjects] = useState([]);
99	  const [loading, setLoading]   = useState(true);
100	  const [editNote, setEditNote] = useState(null);
101	  const [deleteNote, setDeleteNote] = useState(null);
102	  const [showNew, setShowNew]   = useState(false);
103
104	  const load = async () => {
105	    setLoading(true);
106	    try {
107	      const [n, pr] = await Promise.all([api.get('/notes'), api.get('/projects')]);
108	      setNotes(n); setProjects(pr);
109	    } finally { setLoading(false); }
110	  };
111	  useEffect(() => { load(); }, []);
112
113	  const handleSave = async (form) => {
114	    try {
115	      if (form.id) { await api.put(`/notes/${form.id}`, form); toast.success('Note updated'); }
116	      else         { await api.post('/notes', form);            toast.success('Note added');   }
117	      setEditNote(null); setShowNew(false); load();
118	    } catch (e) { toast.error(e.message); }
119	  };
120
121	  const handleDelete = async () => {
122	    await api.delete(`/notes/${deleteNote.id}`); toast.success('Note deleted');
123	    setDeleteNote(null); load();
124	  };
125
126	  return (
127

128	      {/* Header */}
129

130

131
Notes

132	          {!loading &&
{notes.length} note{notes.length !== 1 ? 's' : ''}
}
133

134	        <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5">
135	          <Plus size={14} /> New Note
136	        </button>
137

138
139	      {/* Grid */}
140

141	        {loading ? (
142

143	            {[...Array(6)].map((_, i) =>
)}
144

145	        ) : notes.length === 0 ? (
146

147	            <FileText size={44} className="text-gray-200 dark:text-gray-700" />
148

149
No notes yet

150
Capture ideas, meeting notes, decisions

151

152	            <button onClick={() => setShowNew(true)} className="btn-primary">Create your first note</button>
153

154	        ) : (
155	          /* Masonry-style columns */
156

157	            {notes.map((note, i) => (
158

159	                <NoteCard
160	                  note={note}
161	                  colorClass={NOTE_COLORS[i % NOTE_COLORS.length]}
162	                  onEdit={setEditNote}
163	                  onDelete={setDeleteNote}
164	                />
165

166	            ))}
167

168	        )}
169

170
171	      {(showNew || editNote) && (
172	        <NoteModal note={editNote} projects={projects} onSave={handleSave} onClose={() => { setShowNew(false); setEditNote(null); }} />
173	      )}
174	      {deleteNote && (
175	        <ConfirmDialog title="Delete note?" message={`"${deleteNote.title}"`} onConfirm={handleDelete} onCancel={() => setDeleteNote(null)} />
176	      )}
177

178	  );
179	}
180

import { useState, useEffect } from 'react';
import { Plus, FileText, Trash2, Pencil, Calendar, FolderKanban } from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../api';
import ConfirmDialog from './ConfirmDialog';
import { formatDate } from '../utils';

const NOTE_COLORS = [
  'bg-violet-50 dark:bg-violet-900/20 border-violet-200 dark:border-violet-800',
  'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
  'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800',
  'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800',
  'bg-pink-50 dark:bg-pink-900/20 border-pink-200 dark:border-pink-800',
  'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
];

function NoteModal({ note, projects, onSave, onClose }) {
  const [form, setForm] = useState({
    title: '', body: '', project_id: null,
    date: new Date().toISOString().split('T')[0], ...note,
  });
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  return (

 e.stopPropagation()}>

{note?.id ? 'Edit Note' : 'New Note'}

        <form onSubmit={e => { e.preventDefault(); onSave(form); }} className="px-5 py-4 space-y-3">
          <input className="input font-medium" required placeholder="Title" value={form.title} onChange={e => set('title', e.target.value)} autoFocus />
          <textarea className="input resize-none" rows={7} placeholder="Write your note…" value={form.body} onChange={e => set('body', e.target.value)} />

              Date
              <input type="date" className="input" value={form.date} onChange={e => set('date', e.target.value)} />

              Project
              <select className="select" value={form.project_id || ''} onChange={e => set('project_id', e.target.value || null)}>
                <option value="">No project</option>
                {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              </select>

            <button type="button" onClick={onClose} className="btn-secondary">Cancel</button>
            <button type="submit" className="btn-primary">{note?.id ? 'Save' : 'Add Note'}</button>

        </form>

  );
}

function NoteCard({ note, colorClass, onEdit, onDelete }) {
  const [expanded, setExpanded] = useState(false);
  return (
    <div className={`rounded-2xl border p-4 flex flex-col gap-2 group transition-all duration-200 hover:shadow-hover hover:-translate-y-0.5 ${colorClass}`}>

{note.title}

          <button onClick={() => onEdit(note)} className="p-1.5 rounded-lg hover:bg-white/60 dark:hover:bg-gray-700 text-gray-500 transition-colors">
            <Pencil size={12} />
          </button>
          <button onClick={() => onDelete(note)} className="p-1.5 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 text-gray-400 hover:text-red-500 transition-colors">
            <Trash2 size={12} />
          </button>

      {note.body && (
        <p className={`text-xs text-gray-600 dark:text-gray-400 leading-relaxed whitespace-pre-wrap ${!expanded ? 'line-clamp-4' : ''}`}>
          {note.body}

      )}
      {note.body && note.body.length > 200 && (
        <button onClick={() => setExpanded(!expanded)} className="text-[10px] font-medium text-primary hover:underline self-start">
          {expanded ? 'Show less' : 'Read more'}
        </button>
      )}

        <Calendar size={10} className="text-gray-400 shrink-0" />
        {formatDate(note.date)}
        {note.project_name && (

            <FolderKanban size={9} />{note.project_name}

        )}

  );
}

export default function Notes() {
  const [notes, setNotes]       = useState([]);
  const [projects, setProjects] = useState([]);
  const [loading, setLoading]   = useState(true);
  const [editNote, setEditNote] = useState(null);
  const [deleteNote, setDeleteNote] = useState(null);
  const [showNew, setShowNew]   = useState(false);

  const load = async () => {
    setLoading(true);
    try {
      const [n, pr] = await Promise.all([api.get('/notes'), api.get('/projects')]);
      setNotes(n); setProjects(pr);
    } finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const handleSave = async (form) => {
    try {
      if (form.id) { await api.put(`/notes/${form.id}`, form); toast.success('Note updated'); }
      else         { await api.post('/notes', form);            toast.success('Note added');   }
      setEditNote(null); setShowNew(false); load();
    } catch (e) { toast.error(e.message); }
  };

  const handleDelete = async () => {
    await api.delete(`/notes/${deleteNote.id}`); toast.success('Note deleted');
    setDeleteNote(null); load();
  };

  return (

      {/* Header */}

Notes

          {!loading &&
{notes.length} note{notes.length !== 1 ? 's' : ''}
}

        <button onClick={() => setShowNew(true)} className="btn-primary flex items-center gap-1.5">
          <Plus size={14} /> New Note
        </button>

      {/* Grid */}

        {loading ? (

            {[...Array(6)].map((_, i) =>
)}

        ) : notes.length === 0 ? (

            <FileText size={44} className="text-gray-200 dark:text-gray-700" />

No notes yet

Capture ideas, meeting notes, decisions

            <button onClick={() => setShowNew(true)} className="btn-primary">Create your first note</button>

        ) : (
          /* Masonry-style columns */

            {notes.map((note, i) => (

                <NoteCard
                  note={note}
                  colorClass={NOTE_COLORS[i % NOTE_COLORS.length]}
                  onEdit={setEditNote}
                  onDelete={setDeleteNote}
                />

            ))}

        )}

      {(showNew || editNote) && (
        <NoteModal note={editNote} projects={projects} onSave={handleSave} onClose={() => { setShowNew(false); setEditNote(null); }} />
      )}
      {deleteNote && (
        <ConfirmDialog title="Delete note?" message={`"${deleteNote.title}"`} onConfirm={handleDelete} onCancel={() => setDeleteNote(null)} />
      )}

  );
}


1	import { defineConfig } from 'vite';
2	import react from '@vitejs/plugin-react';
3
4	export default defineConfig({
5	  plugins: [react()],
6	  server: {
7	    port: 5174,
8	    host: '0.0.0.0',   // expose on all interfaces (Tailscale + LAN)
9	    proxy: {
10	      '/api': {
11	        target: 'http://localhost:3001',
12	        changeOrigin: true,
13	      },
14	    },
15	  },
16	  build: {
17	    outDir: '../dist',
18	    emptyOutDir: true,
19	  },
20	});
21

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5174,
    host: '0.0.0.0',   // expose on all interfaces (Tailscale + LAN)
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: '../dist',
    emptyOutDir: true,
  },
});


1	import { useState, useEffect } from 'react';
2	import { Bell, Copy, Check, User, AlertTriangle } from 'lucide-react';
3	import toast from 'react-hot-toast';
4	import api from '../api';
5	import { getPriorityClass, getCategoryClass, CATEGORIES, daysAgo, formatDate } from '../utils';
6
7	export default function FollowUps() {
8	  const [tasks, setTasks]   = useState([]);
9	  const [loading, setLoading] = useState(true);
10	  const [copied, setCopied] = useState(null);
11
12	  useEffect(() => {
13	    api.get('/tasks?follow_up_only=true').then(setTasks).finally(() => setLoading(false));
14	  }, []);
15
16	  const nudge = async (task) => {
17	    const name = task.assignee || 'there';
18	    const msg  = `Hi ${name}, just following up on: "${task.title}". Could you share an update when you get a chance? Thanks!`;
19	    try {
20	      await navigator.clipboard.writeText(msg);
21	      setCopied(task.id);
22	      toast.success('Message copied!');
23	      setTimeout(() => setCopied(null), 2000);
24	    } catch { toast.error('Could not copy to clipboard'); }
25	  };
26
27	  // Group by assignee
28	  const grouped = {};
29	  for (const t of tasks) {
30	    const key = t.assignee || 'Unassigned';
31	    if (!grouped[key]) grouped[key] = [];
32	    grouped[key].push(t);
33	  }
34	  const sortedGroups = Object.entries(grouped).sort((a, b) => b[1].length - a[1].length);
35
36	  return (
37

38	      {/* Header */}
39

40
Follow-ups

41

42	          {loading ? '…' : `${tasks.length} open · grouped by person`}
43

44

45
46

47	        {loading ? (
48

49	            {[...Array(4)].map((_, i) =>
)}
50

51	        ) : tasks.length === 0 ? (
52

53

54	              <Bell size={28} className="text-green-500" />
55

56

57
All clear!

58
No open follow-ups. You're on top of things.

59

60

61	        ) : (
62	          /* Horizontal scroll on mobile, grid on desktop */
63

64	            {sortedGroups.map(([person, personTasks]) => {
65	              const avatarLetter = person === 'Unassigned' ? '?' : person[0].toUpperCase();
66	              const staleCount = personTasks.filter(t => daysAgo(t.created_at) >= 7).length;
67	              return (
68

69	                  {/* Person header */}
70

71

72	                      {avatarLetter}
73

74

75
{person}

76

77	                        {personTasks.length} item{personTasks.length !== 1 ? 's' : ''}
78	                        {staleCount > 0 &&  · {staleCount} stale}
79

80

81

82
83	                  {/* Tasks */}
84

85	                    {personTasks.map(task => {
86	                      const age  = daysAgo(task.created_at);
87	                      const stale = age >= 7;
88	                      return (
89

90

91

92
{task.title}

93

94	                                <span className={`badge text-[10px] py-0 ${getPriorityClass(task.priority)}`}>{task.priority}
95	                                {stale && (
96
97	                                    <AlertTriangle size={9} />{age}d old
98
99	                                )}
100	                                {!stale && age > 0 && {age}d ago}
101	                                {task.due_date && {formatDate(task.due_date)}}
102

103

104	                            <button
105	                              onClick={() => nudge(task)}
106	                              className={`shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium transition-all ${
107	                                copied === task.id
108	                                  ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
109	                                  : 'opacity-0 group-hover:opacity-100 bg-gray-100 dark:bg-gray-800 text-gray-600 hover:bg-primary/10 hover:text-primary'
110	                              }`}>
111	                              {copied === task.id ? <><Check size={10} /> Copied</> : <><Copy size={10} /> Nudge</>}
112	                            </button>
113

114

115	                      );
116	                    })}
117

118

119	              );
120	            })}
121

122	        )}
123

124

125	  );
126	}
127

import { useState, useEffect } from 'react';
import { Bell, Copy, Check, User, AlertTriangle } from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../api';
import { getPriorityClass, getCategoryClass, CATEGORIES, daysAgo, formatDate } from '../utils';

export default function FollowUps() {
  const [tasks, setTasks]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [copied, setCopied] = useState(null);

  useEffect(() => {
    api.get('/tasks?follow_up_only=true').then(setTasks).finally(() => setLoading(false));
  }, []);

  const nudge = async (task) => {
    const name = task.assignee || 'there';
    const msg  = `Hi ${name}, just following up on: "${task.title}". Could you share an update when you get a chance? Thanks!`;
    try {
      await navigator.clipboard.writeText(msg);
      setCopied(task.id);
      toast.success('Message copied!');
      setTimeout(() => setCopied(null), 2000);
    } catch { toast.error('Could not copy to clipboard'); }
  };

  // Group by assignee
  const grouped = {};
  for (const t of tasks) {
    const key = t.assignee || 'Unassigned';
    if (!grouped[key]) grouped[key] = [];
    grouped[key].push(t);
  }
  const sortedGroups = Object.entries(grouped).sort((a, b) => b[1].length - a[1].length);

  return (

      {/* Header */}

Follow-ups

          {loading ? '…' : `${tasks.length} open · grouped by person`}

        {loading ? (

            {[...Array(4)].map((_, i) =>
)}

        ) : tasks.length === 0 ? (

              <Bell size={28} className="text-green-500" />

All clear!

No open follow-ups. You're on top of things.

        ) : (
          /* Horizontal scroll on mobile, grid on desktop */

            {sortedGroups.map(([person, personTasks]) => {
              const avatarLetter = person === 'Unassigned' ? '?' : person[0].toUpperCase();
              const staleCount = personTasks.filter(t => daysAgo(t.created_at) >= 7).length;
              return (

                  {/* Person header */}

                      {avatarLetter}

{person}

                        {personTasks.length} item{personTasks.length !== 1 ? 's' : ''}
                        {staleCount > 0 &&  · {staleCount} stale}

                  {/* Tasks */}

                    {personTasks.map(task => {
                      const age  = daysAgo(task.created_at);
                      const stale = age >= 7;
                      return (

{task.title}

                                <span className={`badge text-[10px] py-0 ${getPriorityClass(task.priority)}`}>{task.priority}
                                {stale && (

                                    <AlertTriangle size={9} />{age}d old

                                )}
                                {!stale && age > 0 && {age}d ago}
                                {task.due_date && {formatDate(task.due_date)}}

                            <button
                              onClick={() => nudge(task)}
                              className={`shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium transition-all ${
                                copied === task.id
                                  ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
                                  : 'opacity-0 group-hover:opacity-100 bg-gray-100 dark:bg-gray-800 text-gray-600 hover:bg-primary/10 hover:text-primary'
                              }`}>
                              {copied === task.id ? <><Check size={10} /> Copied</> : <><Copy size={10} /> Nudge</>}
                            </button>

                      );
                    })}

              );
            })}

        )}

  );
}