1	import { useState, useEffect, useRef } from 'react';
2	import {
3	  Sparkles, Save, Trash2, Lightbulb,
4	  CheckSquare, FileText, FolderKanban, Clock,
5	  User, Calendar, Mic, MicOff,
6	  Paperclip, X, FileImage, FileCode, File
7	} from 'lucide-react';
8	import toast from 'react-hot-toast';
9	import api from '../api';
10	import { CATEGORIES, PRIORITIES, getCategoryClass, getPriorityClass } from '../utils';
11
12	const HISTORY_KEY = 'quicknote_history';
13	function loadHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; } }
14	function pushHistory(item) {
15	  const h = loadHistory();
16	  h.unshift({ ...item, savedAt: new Date().toISOString() });
17	  localStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, 8)));
18	}
19
20	const TYPE_CONFIG = {
21	  task:    { icon: CheckSquare,  color: 'text-primary',    bg: 'bg-primary/10',                        label: 'Task'    },
22	  note:    { icon: FileText,     color: 'text-teal-600',   bg: 'bg-teal-50 dark:bg-teal-900/20',       label: 'Note'    },
23	  project: { icon: FolderKanban, color: 'text-violet-600', bg: 'bg-violet-50 dark:bg-violet-900/20',   label: 'Project' },
24	};
25
26
27	function fileIcon(mime) {
28	  if (!mime) return File;
29	  if (mime.startsWith('image/')) return FileImage;
30	  if (mime === 'application/pdf') return FileCode;
31	  return File;
32	}
33
34	// ── Parsed result card ───────────────────────────────────────────────────────
35	function ParsedCard({ data, onChange, onSave, onDiscard }) {
36	  const TypeCfg = TYPE_CONFIG[data.type] || TYPE_CONFIG.task;
37	  const TypeIcon = TypeCfg.icon;
38
39	  return (
40

41

42	        <div className={`w-8 h-8 rounded-xl ${TypeCfg.bg} flex items-center justify-center`}>
43	          <TypeIcon size={15} className={TypeCfg.color} />
44

45

46
AI Parsed Result

47
Review and edit before saving

48

49	        <select
50	          value={data.type}
51	          onChange={e => onChange('type', e.target.value)}
52	          className="text-xs font-semibold border border-primary/30 rounded-lg px-2.5 py-1.5 bg-white dark:bg-gray-800 text-primary focus:outline-none focus:ring-2 focus:ring-primary/30"
53	        >
54	          <option value="task">Task</option>
55	          <option value="note">Note</option>
56	          <option value="project">Project</option>
57	        </select>
58

59
60

61

62	          Title
63	          <input className="input font-semibold text-base" value={data.title} onChange={e => onChange('title', e.target.value)} placeholder="Title…" />
64

65

66

67	            Category
68	            <select className="select text-sm" value={data.category} onChange={e => onChange('category', e.target.value)}>
69	              {Object.entries(CATEGORIES).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
70	            </select>
71

72

73	            Priority
74	            <select className="select text-sm" value={data.priority} onChange={e => onChange('priority', e.target.value)}>
75	              {PRIORITIES.map(p => <option key={p}>{p}</option>)}
76	            </select>
77

78

79	        {data.type === 'task' && (
80

81

82	              <User size={10} /> Assignee
83	              <input className="input text-sm" value={data.assignee || ''} onChange={e => onChange('assignee', e.target.value)} placeholder="Person name" />
84

85

86	              <Calendar size={10} /> Due Date
87	              <input type="date" className="input text-sm" value={data.due || ''} onChange={e => onChange('due', e.target.value)} />
88

89

90	        )}
91

92	          Details / Notes
93	          <textarea className="input resize-none text-sm" rows={3} value={data.details || ''} onChange={e => onChange('details', e.target.value)} placeholder="Context…" />
94

95	        {data.type === 'task' && (
96

97
98	              <input type="checkbox" className="w-4 h-4 rounded accent-primary" checked={!!data.isFollowUp} onChange={e => onChange('isFollowUp', e.target.checked)} />
99	              Follow-up needed
100
101
102	              <input type="checkbox" className="w-4 h-4 rounded accent-primary" checked={!!data.suggestedTodayFocus} onChange={e => onChange('suggestedTodayFocus', e.target.checked)} />
103	              Pin to today
104
105

106	        )}
107	        {data.projectHint && (
108

109	            <Lightbulb size={13} className="text-amber-600 shrink-0" />
110

111	              Suggested project: {data.projectHint}
112

113

114	        )}
115

116
117

118	        <button onClick={onDiscard} className="btn-ghost flex items-center gap-1.5 text-sm text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
119	          <Trash2 size={13} /> Discard
120	        </button>
121

122	        <button onClick={onSave} className="btn-primary flex items-center gap-2">
123	          <Save size={14} /> Save to Workspace
124	        </button>
125

126

127	  );
128	}
129
130	// ── History card ─────────────────────────────────────────────────────────────
131	function HistoryItem({ item }) {
132	  const TypeCfg = TYPE_CONFIG[item.type] || TYPE_CONFIG.task;
133	  const TypeIcon = TypeCfg.icon;
134	  const when = new Date(item.savedAt);
135	  const timeStr = when.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
136
137	  return (
138

139	      <div className={`w-8 h-8 rounded-xl ${TypeCfg.bg} flex items-center justify-center shrink-0`}>
140	        <TypeIcon size={14} className={TypeCfg.color} />
141

142

143
{item.title}

144

145	          {item.category && (
146	            <span className={`badge text-[10px] py-0 ${getCategoryClass(item.category)}`}>
147	              {CATEGORIES[item.category]?.label?.split(' ')[0]}
148
149	          )}
150	          {item.priority && (
151	            <span className={`badge text-[10px] py-0 ${getPriorityClass(item.priority)}`}>{item.priority}
152	          )}
153	          {timeStr}
154

155

156

157	  );
158	}
159
160	// ── Main component ────────────────────────────────────────────────────────────
161	export default function QuickNote() {
162	  const [text, setText]             = useState('');
163	  const [processing, setProcessing] = useState(false);
164	  const [editData, setEditData]     = useState(null);
165	  const [projects, setProjects]     = useState([]);
166	  const [history, setHistory]       = useState(loadHistory);
167	  const [listening, setListening]   = useState(false);
168	  const [attachedFile, setAttached] = useState(null); // { name, mimeType, dataUrl }
169	  const textareaRef  = useRef(null);
170	  const fileInputRef = useRef(null);
171	  const recognitionRef = useRef(null);
172
173	  useEffect(() => { api.get('/projects').then(setProjects).catch(() => {}); }, []);
174
175	  // ── Voice ────────────────────────────────────────────────────────────────
176	  const startVoice = () => {
177	    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
178	    if (!SR) { toast.error('Voice not supported in this browser'); return; }
179	    const rec = new SR();
180	    rec.continuous = true;
181	    rec.interimResults = true;
182	    rec.lang = 'en-US';
183	    let base = text;
184	    rec.onresult = (e) => {
185	      let interim = '';
186	      let final = '';
187	      for (let i = e.resultIndex; i < e.results.length; i++) {
188	        if (e.results[i].isFinal) final += e.results[i][0].transcript;
189	        else interim += e.results[i][0].transcript;
190	      }
191	      if (final) base = base + (base ? ' ' : '') + final;
192	      setText(base + (interim ? ' ' + interim : ''));
193	    };
194	    rec.onerror = () => { setListening(false); };
195	    rec.onend   = () => { setListening(false); };
196	    recognitionRef.current = rec;
197	    rec.start();
198	    setListening(true);
199	  };
200
201	  const stopVoice = () => {
202	    recognitionRef.current?.stop();
203	    setListening(false);
204	  };
205
206	  // ── File attachment ──────────────────────────────────────────────────────
207	  const handleFile = (e) => {
208	    const file = e.target.files?.[0];
209	    if (!file) return;
210	    const reader = new FileReader();
211	    reader.onload = (ev) => {
212	      setAttached({ name: file.name, mimeType: file.type, dataUrl: ev.target.result });
213	      toast.success(`Attached: ${file.name}`);
214	    };
215	    reader.readAsDataURL(file);
216	    e.target.value = '';
217	  };
218
219	  const removeAttachment = () => setAttached(null);
220
221	  // ── AI processing ────────────────────────────────────────────────────────
222	  const process = async () => {
223	    if (!text.trim() && !attachedFile) { toast.error('Type something or attach a file first'); return; }
224	    setProcessing(true);
225	    try {
226	      let result;
227	      if (attachedFile) {
228	        result = await api.post('/ai/parse-file', {
229	          fileData: attachedFile.dataUrl,
230	          mimeType: attachedFile.mimeType,
231	          fileName: attachedFile.name,
232	          text: text.trim(),
233	        });
234	      } else {
235	        result = await api.post('/ai/parse', { text });
236	      }
237	      setEditData({ ...result });
238	    } catch (e) {
239	      toast.error('AI parsing failed: ' + e.message);
240	    } finally { setProcessing(false); }
241	  };
242
243	  const changeField = (k, v) => setEditData(d => ({ ...d, [k]: v }));
244
245	  // ── Save ─────────────────────────────────────────────────────────────────
246	  const save = async () => {
247	    if (!editData) return;
248	    try {
249	      const d = editData;
250	      if (d.type === 'task') {
251	        await api.post('/tasks', {
252	          title: d.title, notes: d.details, category: d.category, priority: d.priority,
253	          assignee: d.assignee || '', due_date: d.due || '',
254	          is_follow_up: d.isFollowUp ? 1 : 0,
255	          is_today_focus: d.suggestedTodayFocus ? 1 : 0,
256	        });
257	        toast.success('Task saved!');
258	      } else if (d.type === 'note') {
259	        await api.post('/notes', { title: d.title, body: d.details });
260	        toast.success('Note saved!');
261	      } else {
262	        await api.post('/projects', { name: d.title, category: d.category, description: d.details, priority: d.priority });
263	        toast.success('Project created!');
264	      }
265	      pushHistory({ type: d.type, title: d.title, category: d.category, priority: d.priority });
266	      setHistory(loadHistory());
267	      setEditData(null); setText(''); setAttached(null);
268	      textareaRef.current?.focus();
269	    } catch (e) { toast.error(e.message); }
270	  };
271
272	  const discard = () => setEditData(null);
273	const AttachIcon = attachedFile ? fileIcon(attachedFile.mimeType) : Paperclip;
274
275	  return (
276

277
278	      {/* Header — vibrant gradient */}
279

280

281

282	            <Sparkles size={15} className="text-white" />
283

284

285
Quick Note

286
Type, speak, or attach — AI structures it instantly

287

288

289

290
291	      {/* Main content */}
292

293
294	        {/* ── LEFT: Input + Parsed result ────────────────────────────── */}
295

296
297	          {/* Input card — gradient border accent */}
298

299	            {/* Top accent bar */}
300

301
302	            {/* Textarea — 3 rows on mobile, 5 on desktop */}
303	            <textarea
304	              ref={textareaRef}
305	              value={text}
306	              onChange={e => setText(e.target.value)}
307	              onKeyDown={e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') process(); }}
308	              rows={3}
309	              placeholder={"Type anything — task, idea, follow-up, meeting note…"}
310	              className="w-full px-4 pt-4 pb-2 text-sm sm:text-base bg-transparent border-none outline-none resize-none text-gray-900 dark:text-white placeholder-gray-300 dark:placeholder-gray-600 font-medium leading-relaxed sm:rows-5"
311	              style={{ minHeight: '80px' }}
312	            />
313
314	            {/* Attached file pill */}
315	            {attachedFile && (
316

317	                <AttachIcon size={12} className="text-violet-600 shrink-0" />
318	                {attachedFile.name}
319	                <button onClick={removeAttachment} className="text-violet-400 hover:text-red-500 transition-colors">
320	                  <X size={12} />
321	                </button>
322

323	            )}
324
325	            {/* Toolbar */}
326

327
328	              {/* Voice button */}
329	              <button
330	                onClick={listening ? stopVoice : startVoice}
331	                title={listening ? 'Stop recording' : 'Start voice input'}
332	                className={`flex items-center gap-1 px-2.5 py-1.5 rounded-xl text-xs font-medium transition-all ${
333	                  listening
334	                    ? 'bg-red-500 text-white animate-pulse shadow-md'
335	                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
336	                }`}
337	              >
338	                {listening ? <MicOff size={12} /> : <Mic size={12} />}
339	                {listening ? 'Stop' : 'Voice'}
340	                {listening ? 'Stop' : ''}
341	              </button>
342
343	              {/* Attach file button */}
344	              <button
345	                onClick={() => fileInputRef.current?.click()}
346	                title="Attach file — image, PDF, or text"
347	                className={`flex items-center gap-1 px-2.5 py-1.5 rounded-xl text-xs font-medium transition-all ${
348	                  attachedFile
349	                    ? 'bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300'
350	                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
351	                }`}
352	              >
353	                <Paperclip size={12} />
354	                {attachedFile ? 'Change' : 'Attach'}
355	              </button>
356	              <input
357	                ref={fileInputRef}
358	                type="file"
359	                className="hidden"
360	                accept="image/*,.pdf,.txt,.csv,.md"
361	                onChange={handleFile}
362	              />
363
364

365
366	              {(text.trim() || attachedFile) && !processing && !editData && (
367	                <button onClick={() => { setText(''); setAttached(null); discard(); }} className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 px-2 transition-colors">
368	                  Clear
369	                </button>
370	              )}
371
372	              <button
373	                onClick={process}
374	                disabled={(!text.trim() && !attachedFile) || processing || !!editData}
375	                className="btn-primary flex items-center gap-1.5 py-1.5 px-4 text-xs sm:text-sm disabled:opacity-40 bg-gradient-to-r from-violet-600 to-primary border-0"
376	              >
377	                {processing ? (
378	                  <> Parsing…</>
379	                ) : (
380	                  <><Sparkles size={13} /> Process</>
381	                )}
382	              </button>
383

384
385
386

387
388	          {/* Parsed result */}
389	          {editData && (
390	            <ParsedCard
391	              data={editData}
392	              onChange={changeField}
393	              projects={projects}
394	              onSave={save}
395	              onDiscard={discard}
396	            />
397	          )}
398
399	          {/* Empty state */}
400	          {!editData && !processing && (
401

402	              <Sparkles size={32} className="text-gray-300 dark:text-gray-700 mb-3" />
403
Type, speak, or attach a file — then click Process

404
Claude will parse it into a task, note, or project

405

406	          )}
407

408
409	        {/* ── RIGHT: History ──────────────────────────────────────────── */}
410

411

412
Recent Captures

413
Last {history.length} items saved via Quick Note

414

415
416	          {history.length === 0 ? (
417

418	              <Clock size={28} className="text-gray-200 dark:text-gray-700 mb-3" />
419
Nothing captured yet

420
Your saved items will appear here

421

422	          ) : (
423

424	              {history.map((item, i) => <HistoryItem key={i} item={item} />)}
425

426	          )}
427
428	          {/* Tip box */}
429

430
Tips

431

432
• ⌘ Enter to process instantly

433
• Click Voice to dictate hands-free

434
• Attach a PDF, image, or text — AI reads it

435

436

437

438

439

440	  );
441	}
442

import { useState, useEffect, useRef } from 'react';
import {
  Sparkles, Save, Trash2, Lightbulb,
  CheckSquare, FileText, FolderKanban, Clock,
  User, Calendar, Mic, MicOff,
  Paperclip, X, FileImage, FileCode, File
} from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../api';
import { CATEGORIES, PRIORITIES, getCategoryClass, getPriorityClass } from '../utils';

const HISTORY_KEY = 'quicknote_history';
function loadHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; } }
function pushHistory(item) {
  const h = loadHistory();
  h.unshift({ ...item, savedAt: new Date().toISOString() });
  localStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, 8)));
}

const TYPE_CONFIG = {
  task:    { icon: CheckSquare,  color: 'text-primary',    bg: 'bg-primary/10',                        label: 'Task'    },
  note:    { icon: FileText,     color: 'text-teal-600',   bg: 'bg-teal-50 dark:bg-teal-900/20',       label: 'Note'    },
  project: { icon: FolderKanban, color: 'text-violet-600', bg: 'bg-violet-50 dark:bg-violet-900/20',   label: 'Project' },
};

function fileIcon(mime) {
  if (!mime) return File;
  if (mime.startsWith('image/')) return FileImage;
  if (mime === 'application/pdf') return FileCode;
  return File;
}

// ── Parsed result card ───────────────────────────────────────────────────────
function ParsedCard({ data, onChange, onSave, onDiscard }) {
  const TypeCfg = TYPE_CONFIG[data.type] || TYPE_CONFIG.task;
  const TypeIcon = TypeCfg.icon;

  return (

        <div className={`w-8 h-8 rounded-xl ${TypeCfg.bg} flex items-center justify-center`}>
          <TypeIcon size={15} className={TypeCfg.color} />

AI Parsed Result

Review and edit before saving

        <select
          value={data.type}
          onChange={e => onChange('type', e.target.value)}
          className="text-xs font-semibold border border-primary/30 rounded-lg px-2.5 py-1.5 bg-white dark:bg-gray-800 text-primary focus:outline-none focus:ring-2 focus:ring-primary/30"
        >
          <option value="task">Task</option>
          <option value="note">Note</option>
          <option value="project">Project</option>
        </select>

          Title
          <input className="input font-semibold text-base" value={data.title} onChange={e => onChange('title', e.target.value)} placeholder="Title…" />

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

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

        {data.type === 'task' && (

              <User size={10} /> Assignee
              <input className="input text-sm" value={data.assignee || ''} onChange={e => onChange('assignee', e.target.value)} placeholder="Person name" />

              <Calendar size={10} /> Due Date
              <input type="date" className="input text-sm" value={data.due || ''} onChange={e => onChange('due', e.target.value)} />

        )}

          Details / Notes
          <textarea className="input resize-none text-sm" rows={3} value={data.details || ''} onChange={e => onChange('details', e.target.value)} placeholder="Context…" />

        {data.type === 'task' && (

              <input type="checkbox" className="w-4 h-4 rounded accent-primary" checked={!!data.isFollowUp} onChange={e => onChange('isFollowUp', e.target.checked)} />
              Follow-up needed

              <input type="checkbox" className="w-4 h-4 rounded accent-primary" checked={!!data.suggestedTodayFocus} onChange={e => onChange('suggestedTodayFocus', e.target.checked)} />
              Pin to today

        )}
        {data.projectHint && (

            <Lightbulb size={13} className="text-amber-600 shrink-0" />

              Suggested project: {data.projectHint}

        )}

        <button onClick={onDiscard} className="btn-ghost flex items-center gap-1.5 text-sm text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
          <Trash2 size={13} /> Discard
        </button>

        <button onClick={onSave} className="btn-primary flex items-center gap-2">
          <Save size={14} /> Save to Workspace
        </button>

  );
}

// ── History card ─────────────────────────────────────────────────────────────
function HistoryItem({ item }) {
  const TypeCfg = TYPE_CONFIG[item.type] || TYPE_CONFIG.task;
  const TypeIcon = TypeCfg.icon;
  const when = new Date(item.savedAt);
  const timeStr = when.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });

  return (

      <div className={`w-8 h-8 rounded-xl ${TypeCfg.bg} flex items-center justify-center shrink-0`}>
        <TypeIcon size={14} className={TypeCfg.color} />

{item.title}

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

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

  );
}

// ── Main component ────────────────────────────────────────────────────────────
export default function QuickNote() {
  const [text, setText]             = useState('');
  const [processing, setProcessing] = useState(false);
  const [editData, setEditData]     = useState(null);
  const [projects, setProjects]     = useState([]);
  const [history, setHistory]       = useState(loadHistory);
  const [listening, setListening]   = useState(false);
  const [attachedFile, setAttached] = useState(null); // { name, mimeType, dataUrl }
  const textareaRef  = useRef(null);
  const fileInputRef = useRef(null);
  const recognitionRef = useRef(null);

  useEffect(() => { api.get('/projects').then(setProjects).catch(() => {}); }, []);

  // ── Voice ────────────────────────────────────────────────────────────────
  const startVoice = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { toast.error('Voice not supported in this browser'); return; }
    const rec = new SR();
    rec.continuous = true;
    rec.interimResults = true;
    rec.lang = 'en-US';
    let base = text;
    rec.onresult = (e) => {
      let interim = '';
      let final = '';
      for (let i = e.resultIndex; i < e.results.length; i++) {
        if (e.results[i].isFinal) final += e.results[i][0].transcript;
        else interim += e.results[i][0].transcript;
      }
      if (final) base = base + (base ? ' ' : '') + final;
      setText(base + (interim ? ' ' + interim : ''));
    };
    rec.onerror = () => { setListening(false); };
    rec.onend   = () => { setListening(false); };
    recognitionRef.current = rec;
    rec.start();
    setListening(true);
  };

  const stopVoice = () => {
    recognitionRef.current?.stop();
    setListening(false);
  };

  // ── File attachment ──────────────────────────────────────────────────────
  const handleFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      setAttached({ name: file.name, mimeType: file.type, dataUrl: ev.target.result });
      toast.success(`Attached: ${file.name}`);
    };
    reader.readAsDataURL(file);
    e.target.value = '';
  };

  const removeAttachment = () => setAttached(null);

  // ── AI processing ────────────────────────────────────────────────────────
  const process = async () => {
    if (!text.trim() && !attachedFile) { toast.error('Type something or attach a file first'); return; }
    setProcessing(true);
    try {
      let result;
      if (attachedFile) {
        result = await api.post('/ai/parse-file', {
          fileData: attachedFile.dataUrl,
          mimeType: attachedFile.mimeType,
          fileName: attachedFile.name,
          text: text.trim(),
        });
      } else {
        result = await api.post('/ai/parse', { text });
      }
      setEditData({ ...result });
    } catch (e) {
      toast.error('AI parsing failed: ' + e.message);
    } finally { setProcessing(false); }
  };

  const changeField = (k, v) => setEditData(d => ({ ...d, [k]: v }));

  // ── Save ─────────────────────────────────────────────────────────────────
  const save = async () => {
    if (!editData) return;
    try {
      const d = editData;
      if (d.type === 'task') {
        await api.post('/tasks', {
          title: d.title, notes: d.details, category: d.category, priority: d.priority,
          assignee: d.assignee || '', due_date: d.due || '',
          is_follow_up: d.isFollowUp ? 1 : 0,
          is_today_focus: d.suggestedTodayFocus ? 1 : 0,
        });
        toast.success('Task saved!');
      } else if (d.type === 'note') {
        await api.post('/notes', { title: d.title, body: d.details });
        toast.success('Note saved!');
      } else {
        await api.post('/projects', { name: d.title, category: d.category, description: d.details, priority: d.priority });
        toast.success('Project created!');
      }
      pushHistory({ type: d.type, title: d.title, category: d.category, priority: d.priority });
      setHistory(loadHistory());
      setEditData(null); setText(''); setAttached(null);
      textareaRef.current?.focus();
    } catch (e) { toast.error(e.message); }
  };

  const discard = () => setEditData(null);
const AttachIcon = attachedFile ? fileIcon(attachedFile.mimeType) : Paperclip;

  return (

      {/* Header — vibrant gradient */}

            <Sparkles size={15} className="text-white" />

Quick Note

Type, speak, or attach — AI structures it instantly

      {/* Main content */}

        {/* ── LEFT: Input + Parsed result ────────────────────────────── */}

          {/* Input card — gradient border accent */}

            {/* Top accent bar */}

            {/* Textarea — 3 rows on mobile, 5 on desktop */}
            <textarea
              ref={textareaRef}
              value={text}
              onChange={e => setText(e.target.value)}
              onKeyDown={e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') process(); }}
              rows={3}
              placeholder={"Type anything — task, idea, follow-up, meeting note…"}
              className="w-full px-4 pt-4 pb-2 text-sm sm:text-base bg-transparent border-none outline-none resize-none text-gray-900 dark:text-white placeholder-gray-300 dark:placeholder-gray-600 font-medium leading-relaxed sm:rows-5"
              style={{ minHeight: '80px' }}
            />

            {/* Attached file pill */}
            {attachedFile && (

                <AttachIcon size={12} className="text-violet-600 shrink-0" />
                {attachedFile.name}
                <button onClick={removeAttachment} className="text-violet-400 hover:text-red-500 transition-colors">
                  <X size={12} />
                </button>

            )}

            {/* Toolbar */}

              {/* Voice button */}
              <button
                onClick={listening ? stopVoice : startVoice}
                title={listening ? 'Stop recording' : 'Start voice input'}
                className={`flex items-center gap-1 px-2.5 py-1.5 rounded-xl text-xs font-medium transition-all ${
                  listening
                    ? 'bg-red-500 text-white animate-pulse shadow-md'
                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
                }`}
              >
                {listening ? <MicOff size={12} /> : <Mic size={12} />}
                {listening ? 'Stop' : 'Voice'}
                {listening ? 'Stop' : ''}
              </button>

              {/* Attach file button */}
              <button
                onClick={() => fileInputRef.current?.click()}
                title="Attach file — image, PDF, or text"
                className={`flex items-center gap-1 px-2.5 py-1.5 rounded-xl text-xs font-medium transition-all ${
                  attachedFile
                    ? 'bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300'
                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
                }`}
              >
                <Paperclip size={12} />
                {attachedFile ? 'Change' : 'Attach'}
              </button>
              <input
                ref={fileInputRef}
                type="file"
                className="hidden"
                accept="image/*,.pdf,.txt,.csv,.md"
                onChange={handleFile}
              />

              {(text.trim() || attachedFile) && !processing && !editData && (
                <button onClick={() => { setText(''); setAttached(null); discard(); }} className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 px-2 transition-colors">
                  Clear
                </button>
              )}

              <button
                onClick={process}
                disabled={(!text.trim() && !attachedFile) || processing || !!editData}
                className="btn-primary flex items-center gap-1.5 py-1.5 px-4 text-xs sm:text-sm disabled:opacity-40 bg-gradient-to-r from-violet-600 to-primary border-0"
              >
                {processing ? (
                  <> Parsing…</>
                ) : (
                  <><Sparkles size={13} /> Process</>
                )}
              </button>

          {/* Parsed result */}
          {editData && (
            <ParsedCard
              data={editData}
              onChange={changeField}
              projects={projects}
              onSave={save}
              onDiscard={discard}
            />
          )}

          {/* Empty state */}
          {!editData && !processing && (

              <Sparkles size={32} className="text-gray-300 dark:text-gray-700 mb-3" />

Type, speak, or attach a file — then click Process

Claude will parse it into a task, note, or project

          )}

        {/* ── RIGHT: History ──────────────────────────────────────────── */}

Recent Captures

Last {history.length} items saved via Quick Note

          {history.length === 0 ? (

              <Clock size={28} className="text-gray-200 dark:text-gray-700 mb-3" />

Nothing captured yet

Your saved items will appear here

          ) : (

              {history.map((item, i) => <HistoryItem key={i} item={item} />)}

          )}

          {/* Tip box */}

Tips

• ⌘ Enter to process instantly

• Click Voice to dictate hands-free

• Attach a PDF, image, or text — AI reads it

  );
}


Let me check the remaining components and server routes.


1	import { useState, useEffect } from 'react';
2	import { BarChart2, CheckCircle2, TrendingUp, AlertCircle, Clock, FolderKanban, RefreshCw, Sparkles } from 'lucide-react';
3	import { Link } from 'react-router-dom';
4	import toast from 'react-hot-toast';
5	import api from '../api';
6	import { getPriorityClass, getStatusClass, formatDate } from '../utils';
7	import ConfirmDialog from './ConfirmDialog';
8
9	export default function WeeklyReview() {
10	  const [data, setData]             = useState(null);
11	  const [loading, setLoading]       = useState(true);
12	  const [confirmReset, setConfirm]  = useState(false);
13
14	  const load = () => {
15	    setLoading(true);
16	    api.get('/weekly-review').then(setData).finally(() => setLoading(false));
17	  };
18	  useEffect(() => { load(); }, []);
19
20	  const handleReset = async () => {
21	    try {
22	      await api.post('/weekly-review/reset');
23	      toast.success('New week started! Today focus cleared.');
24	      setConfirm(false); load();
25	    } catch (e) { toast.error(e.message); }
26	  };
27
28	  const weekStart = data?.weekStart
29	    ? new Date(data.weekStart).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
30	    : '';
31
32	  return (
33

34	      {/* Header */}
35

36

37

38
Weekly Review

39	            {weekStart &&
Week of {weekStart}
}
40

41

42	            <button onClick={load} className="btn-secondary flex items-center gap-1.5 py-2 text-sm">
43	              <RefreshCw size={13} /> Refresh
44	            </button>
45	            <button onClick={() => setConfirm(true)} className="btn-primary flex items-center gap-1.5 py-2 text-sm">
46	              <Sparkles size={13} /> Start New Week
47	            </button>
48

49

50

51
52

53	        {loading ? (
54

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

57	        ) : data && (
58	          <>
59	            {/* ── Top stat cards — horizontal ─────────────────── */}
60

61	              {[
62	                { icon: CheckCircle2, label: 'Completed',   value: data.completedThisWeek.length, color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20',   bar: 'bg-green-500' },
63	                { icon: TrendingUp,   label: 'Added',        value: data.addedThisWeek.length,     color: 'text-blue-600',  bg: 'bg-blue-50 dark:bg-blue-900/20',     bar: 'bg-blue-500'  },
64	                { icon: AlertCircle,  label: 'Overdue',      value: data.overdue.length,           color: 'text-red-600',   bg: 'bg-red-50 dark:bg-red-900/20',       bar: 'bg-red-500'   },
65	                { icon: Clock,        label: 'Stale Follow-ups', value: data.staleFollowUps.length, color: 'text-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/20', bar: 'bg-amber-500' },
66	              ].map(({ icon: Icon, label, value, color, bg, bar }) => (
67	                <div key={label} className={`${bg} rounded-2xl p-4 flex flex-col gap-2`}>
68	                  <Icon size={18} className={color} />
69	                  <p className={`text-3xl font-bold ${color}`}>{value}

70
{label}

71

72	                    <div className={`h-full ${bar} rounded-full transition-all duration-700`}
73	                      style={{ width: `${Math.min(value * 10, 100)}%` }} />
74

75

76	              ))}
77

78
79	            {/* ── Detail sections — horizontal 2-col grid on desktop ── */}
80

81
82	              {/* Completed */}
83
 (
86

87	                    <CheckCircle2 size={13} className="text-green-500 shrink-0 mt-0.5" />
88

89
{t.title}

90

91	                        <span className={`badge text-[10px] py-0 ${getPriorityClass(t.priority)}`}>{t.priority}
92

93

94

95	                )}
96	              />
97
98	              {/* Overdue */}
99
 (
102

103	                    <AlertCircle size={13} className="text-red-500 shrink-0 mt-0.5" />
104

105
{t.title}

106

107	                        <span className={`badge text-[10px] py-0 ${getPriorityClass(t.priority)}`}>{t.priority}
108	                        Due {formatDate(t.due_date)}
109

110

111	                    <Link to="/tasks" className="text-[10px] text-primary hover:underline shrink-0 mt-0.5">View</Link>
112

113	                )}
114	              />
115
116	              {/* Stale follow-ups */}
117
 (
120

121	                    <Clock size={13} className="text-amber-500 shrink-0 mt-0.5" />
122
{t.title}

123

124	                )}
125	              />
126
127	              {/* Inactive projects */}
128
 (
131

132	                    <FolderKanban size={13} className="text-gray-400 shrink-0" />
133
{p.name}

134

135	                      <span className={`badge text-[10px] py-0 ${getPriorityClass(p.priority)}`}>{p.priority}
136	                      <Link to={`/projects/${p.id}`} className="text-[10px] text-primary hover:underline">Open</Link>
137

138

139	                )}
140	              />
141

142
143	            {/* Added this week - full width */}
144
 (
147

148	                  <span className={`badge text-[10px] py-0 ${getPriorityClass(t.priority)}`}>{t.priority}
149
{t.title}

150

151	              )}
152	              wrapContent
153	            />
154	          </>
155	        )}
156

157
158	      {confirmReset && (
159	        <ConfirmDialog
160	          title="Start new week?"
161	          message="This clears all today-focus pins so you can choose fresh focus items."
162	          onConfirm={handleReset} onCancel={() => setConfirm(false)} danger={false}
163	        />
164	      )}
165

166	  );
167	}
168
169	function Section({ icon: Icon, color, title, items, empty, renderItem, wrapContent }) {
170	  return (
171

172

173	        <Icon size={14} className={color} />
174
{title}

175	        {items.length}
176

177	      {items.length === 0
178	        ?
{empty}

179	        : <div className={`px-4 py-1 max-h-56 overflow-y-auto ${wrapContent ? 'flex flex-wrap py-2' : ''}`}>
180	            {items.map(renderItem)}
181

182	      }
183

184	  );
185	}
186

import { useState, useEffect } from 'react';
import { BarChart2, CheckCircle2, TrendingUp, AlertCircle, Clock, FolderKanban, RefreshCw, Sparkles } from 'lucide-react';
import { Link } from 'react-router-dom';
import toast from 'react-hot-toast';
import api from '../api';
import { getPriorityClass, getStatusClass, formatDate } from '../utils';
import ConfirmDialog from './ConfirmDialog';

export default function WeeklyReview() {
  const [data, setData]             = useState(null);
  const [loading, setLoading]       = useState(true);
  const [confirmReset, setConfirm]  = useState(false);

  const load = () => {
    setLoading(true);
    api.get('/weekly-review').then(setData).finally(() => setLoading(false));
  };
  useEffect(() => { load(); }, []);

  const handleReset = async () => {
    try {
      await api.post('/weekly-review/reset');
      toast.success('New week started! Today focus cleared.');
      setConfirm(false); load();
    } catch (e) { toast.error(e.message); }
  };

  const weekStart = data?.weekStart
    ? new Date(data.weekStart).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
    : '';

  return (

      {/* Header */}

Weekly Review

            {weekStart &&
Week of {weekStart}
}

            <button onClick={load} className="btn-secondary flex items-center gap-1.5 py-2 text-sm">
              <RefreshCw size={13} /> Refresh
            </button>
            <button onClick={() => setConfirm(true)} className="btn-primary flex items-center gap-1.5 py-2 text-sm">
              <Sparkles size={13} /> Start New Week
            </button>

        {loading ? (

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

        ) : data && (
          <>
            {/* ── Top stat cards — horizontal ─────────────────── */}

              {[
                { icon: CheckCircle2, label: 'Completed',   value: data.completedThisWeek.length, color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20',   bar: 'bg-green-500' },
                { icon: TrendingUp,   label: 'Added',        value: data.addedThisWeek.length,     color: 'text-blue-600',  bg: 'bg-blue-50 dark:bg-blue-900/20',     bar: 'bg-blue-500'  },
                { icon: AlertCircle,  label: 'Overdue',      value: data.overdue.length,           color: 'text-red-600',   bg: 'bg-red-50 dark:bg-red-900/20',       bar: 'bg-red-500'   },
                { icon: Clock,        label: 'Stale Follow-ups', value: data.staleFollowUps.length, color: 'text-amber-600', bg: 'bg-amber-50 dark:bg-amber-900/20', bar: 'bg-amber-500' },
              ].map(({ icon: Icon, label, value, color, bg, bar }) => (
                <div key={label} className={`${bg} rounded-2xl p-4 flex flex-col gap-2`}>
                  <Icon size={18} className={color} />
                  <p className={`text-3xl font-bold ${color}`}>{value}

{label}

                    <div className={`h-full ${bar} rounded-full transition-all duration-700`}
                      style={{ width: `${Math.min(value * 10, 100)}%` }} />

              ))}

            {/* ── Detail sections — horizontal 2-col grid on desktop ── */}

              {/* Completed */}

 (

                    <CheckCircle2 size={13} className="text-green-500 shrink-0 mt-0.5" />

{t.title}

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

                )}
              />

              {/* Overdue */}

 (

                    <AlertCircle size={13} className="text-red-500 shrink-0 mt-0.5" />

{t.title}

                        <span className={`badge text-[10px] py-0 ${getPriorityClass(t.priority)}`}>{t.priority}
                        Due {formatDate(t.due_date)}

                    <Link to="/tasks" className="text-[10px] text-primary hover:underline shrink-0 mt-0.5">View</Link>

                )}
              />

              {/* Stale follow-ups */}

 (

                    <Clock size={13} className="text-amber-500 shrink-0 mt-0.5" />

{t.title}

                )}
              />

              {/* Inactive projects */}

 (

                    <FolderKanban size={13} className="text-gray-400 shrink-0" />

{p.name}

                      <span className={`badge text-[10px] py-0 ${getPriorityClass(p.priority)}`}>{p.priority}
                      <Link to={`/projects/${p.id}`} className="text-[10px] text-primary hover:underline">Open</Link>

                )}
              />

            {/* Added this week - full width */}

 (

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

{t.title}

              )}
              wrapContent
            />
          </>
        )}

      {confirmReset && (
        <ConfirmDialog
          title="Start new week?"
          message="This clears all today-focus pins so you can choose fresh focus items."
          onConfirm={handleReset} onCancel={() => setConfirm(false)} danger={false}
        />
      )}

  );
}

function Section({ icon: Icon, color, title, items, empty, renderItem, wrapContent }) {
  return (

        <Icon size={14} className={color} />

{title}

        {items.length}

      {items.length === 0
        ?
{empty}

        : <div className={`px-4 py-1 max-h-56 overflow-y-auto ${wrapContent ? 'flex flex-wrap py-2' : ''}`}>
            {items.map(renderItem)}

      }

  );
}


1	import { useState, useEffect } from 'react';
2	import { useParams, useNavigate } from 'react-router-dom';
3	import {
4	  ArrowLeft, Pencil, Plus, Trash2, FileText, CheckSquare,
5	  Link2, Search, X, CheckCircle2, Clock, Loader2, PauseCircle,
6	  FolderKanban, AlignLeft, RefreshCw, ChevronRight, Calendar, User
7	} from 'lucide-react';
8	import toast from 'react-hot-toast';
9	import api from '../api';
10	import TaskCard from './TaskCard';
11	import TaskModal from './TaskModal';
12	import ConfirmDialog from './ConfirmDialog';
13	import { CATEGORIES, STATUSES, PRIORITIES, getCategoryClass, getPriorityClass, getStatusClass, formatDate } from '../utils';
14
15	const CATEGORY_ACCENT = {
16	  ai:        'from-blue-500 to-cyan-500',
17	  vendor:    'from-purple-500 to-violet-500',
18	  gov:       'from-amber-500 to-yellow-500',
19	  people:    'from-teal-500 to-emerald-500',
20	  events:    'from-orange-500 to-red-400',
21	  strategic: 'from-green-500 to-teal-500',
22	  adhoc:     'from-red-500 to-rose-500',
23	};
24
25	const CATEGORY_BG = {
26	  ai:        'bg-blue-50 dark:bg-blue-900/20',
27	  vendor:    'bg-purple-50 dark:bg-purple-900/20',
28	  gov:       'bg-amber-50 dark:bg-amber-900/20',
29	  people:    'bg-teal-50 dark:bg-teal-900/20',
30	  events:    'bg-orange-50 dark:bg-orange-900/20',
31	  strategic: 'bg-green-50 dark:bg-green-900/20',
32	  adhoc:     'bg-red-50 dark:bg-red-900/20',
33	};
34
35	const STATUS_META = {
36	  Open:          { icon: Clock,        color: 'text-gray-500',  bg: 'bg-gray-100 dark:bg-gray-800'         },
37	  'In Progress': { icon: Loader2,      color: 'text-blue-600',  bg: 'bg-blue-100 dark:bg-blue-900/30'      },
38	  Waiting:       { icon: PauseCircle,  color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30'    },
39	  Done:          { icon: CheckCircle2, color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/30'    },
40	};
41
42	function InlineEdit({ value, onSave, multiline = false, placeholder = '', className = '' }) {
43	  const [editing, setEditing] = useState(false);
44	  const [val, setVal] = useState(value || '');
45	  useEffect(() => setVal(value || ''), [value]);
46	  const save = () => { setEditing(false); if (val !== value) onSave(val); };
47	  if (!editing) return (
48	     setEditing(true)}
49	      className={`group cursor-text flex items-start gap-1.5 ${className}`}>
50
51	        {val || placeholder}
52
53	      <Pencil size={11} className="opacity-0 group-hover:opacity-40 shrink-0 mt-0.5 text-gray-400" />
54
55	  );
56	  return multiline
57	    ? <textarea autoFocus className="input w-full resize-none text-sm" rows={4} value={val}
58	        onChange={e => setVal(e.target.value)} onBlur={save} />
59	    : <input autoFocus className="input w-full text-sm" value={val}
60	        onChange={e => setVal(e.target.value)} onBlur={save} onKeyDown={e => e.key === 'Enter' && save()} />;
61	}
62
63	function InfoRow({ icon: Icon, label, children }) {
64	  return (
65

66

67	        <Icon size={11} />{label}
68

69
{children}

70

71	  );
72	}
73
74	function NoteForm({ projectId, onSave, onCancel }) {
75	  const [title, setTitle] = useState('');
76	  const [body, setBody] = useState('');
77	  const submit = async (e) => {
78	    e.preventDefault();
79	    if (!title.trim()) return;
80	    await onSave({ title, body, project_id: projectId, date: new Date().toISOString().split('T')[0] });
81	  };
82	  return (
83	    <form onSubmit={submit} className="bg-white dark:bg-gray-900 rounded-2xl border-2 border-dashed border-primary/30 p-4 space-y-3">
84	      <input className="input font-medium" placeholder="Note title…" value={title} onChange={e => setTitle(e.target.value)} autoFocus required />
85	      <textarea className="input resize-none text-sm" rows={4} placeholder="Write your note…" value={body} onChange={e => setBody(e.target.value)} />
86

87	        <button type="button" onClick={onCancel} className="btn-secondary text-xs py-1.5 px-3">Cancel</button>
88	        <button type="submit" className="btn-primary text-xs py-1.5 px-3">Save Note</button>
89

90	    </form>
91	  );
92	}
93
94	export default function ProjectDetail() {
95	  const { id } = useParams();
96	  const navigate = useNavigate();
97	  const [project, setProject]       = useState(null);
98	  const [tasks, setTasks]           = useState([]);
99	  const [notes, setNotes]           = useState([]);
100	  const [projects, setProjects]     = useState([]);
101	  const [tab, setTab]               = useState('tasks');
102	  const [loading, setLoading]       = useState(true);
103	  const [editTask, setEditTask]     = useState(null);
104	  const [deleteTask, setDeleteTask] = useState(null);
105	  const [deleteNote, setDeleteNote] = useState(null);
106	  const [showNewTask, setShowNewTask]   = useState(false);
107	  const [showNewNote, setShowNewNote]   = useState(false);
108	  const [showLinkTask, setShowLinkTask] = useState(false);
109	  const [allTasks, setAllTasks]     = useState([]);
110	  const [linkSearch, setLinkSearch] = useState('');
111
112	  const load = async () => {
113	    try {
114	      const [p, t, n, pr] = await Promise.all([
115	        api.get(`/projects/${id}`),
116	        api.get(`/tasks?project_id=${id}`),
117	        api.get(`/notes?project_id=${id}`),
118	        api.get('/projects'),
119	      ]);
120	      setProject(p); setTasks(t); setNotes(n); setProjects(pr);
121	    } catch { navigate('/projects'); }
122	    finally { setLoading(false); }
123	  };
124
125	  useEffect(() => { load(); }, [id]);
126
127	  const updateProject = async (field, value) => {
128	    const updated = { ...project, [field]: value };
129	    setProject(updated);
130	    await api.put(`/projects/${id}`, updated);
131	  };
132
133	  const handleSaveTask = async (form) => {
134	    try {
135	      if (editTask?.id) { await api.put(`/tasks/${editTask.id}`, { ...form, project_id: id }); toast.success('Task updated'); }
136	      else { await api.post('/tasks', { ...form, project_id: id }); toast.success('Task added'); }
137	      setEditTask(null); setShowNewTask(false); load();
138	    } catch (e) { toast.error(e.message); }
139	  };
140
141	  const handleSaveNote = async (form) => {
142	    try { await api.post('/notes', form); toast.success('Note saved'); setShowNewNote(false); load(); }
143	    catch (e) { toast.error(e.message); }
144	  };
145
146	  const handleDeleteTask = async () => {
147	    await api.delete(`/tasks/${deleteTask.id}`); toast.success('Task deleted');
148	    setDeleteTask(null); load();
149	  };
150
151	  const handleDeleteNote = async () => {
152	    await api.delete(`/notes/${deleteNote.id}`); toast.success('Note deleted');
153	    setDeleteNote(null); load();
154	  };
155
156	  const toggleFocus = async (task) => { await api.patch(`/tasks/${task.id}/focus`); load(); };
157
158	  const openLinkTask = async () => {
159	    const all = await api.get('/tasks');
160	    setAllTasks(all.filter(t => t.project_id != id && t.status !== 'Done'));
161	    setLinkSearch(''); setShowLinkTask(true);
162	  };
163
164	  const linkTask = async (task) => {
165	    await api.put(`/tasks/${task.id}`, { ...task, project_id: id });
166	    toast.success(`Linked to project`);
167	    setAllTasks(prev => prev.filter(t => t.id !== task.id));
168	    load();
169	  };
170
171	  if (loading) return (
172

173

174

175

176

177

178

179

180

181

182

183

184	  );
185
186	  if (!project) return null;
187
188	  const openTasks  = tasks.filter(t => t.status !== 'Done').length;
189	  const doneTasks  = tasks.filter(t => t.status === 'Done').length;
190	  const StatusIcon = STATUS_META[project.status]?.icon || Clock;
191	  const statusColor = STATUS_META[project.status]?.color || 'text-gray-500';
192	  const statusBg    = STATUS_META[project.status]?.bg    || 'bg-gray-100';
193
194	  return (
195

196
197	      {/* Colored accent bar */}
198	      <div className={`h-1.5 w-full bg-gradient-to-r shrink-0 ${CATEGORY_ACCENT[project.category] || 'from-gray-400 to-gray-500'}`} />
199
200	      {/* Header */}
201	      <div className={`shrink-0 px-5 pt-4 pb-5 ${CATEGORY_BG[project.category] || 'bg-white dark:bg-gray-900'} border-b border-black/5 dark:border-white/5`}>
202	        {/* Back + actions */}
203

204	          <button onClick={() => navigate('/projects')}
205	            className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors font-medium">
206	            <ArrowLeft size={15} /> Projects
207	          </button>
208	          <button onClick={() => load()}
209	            className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-600 transition-colors">
210	            <RefreshCw size={12} />
211	          </button>
212

213
214	        {/* Category + title */}
215

216	          <div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 bg-white/70 dark:bg-gray-800/70 shadow-sm`}>
217	            <FolderKanban size={18} className={getCategoryClass(project.category).split(' ').find(c => c.startsWith('text-')) || 'text-gray-500'} />
218

219

220

221	              <InlineEdit value={project.name} onSave={v => updateProject('name', v)} placeholder="Project name" />
222

223

224

225
226	        {/* Status / Priority / Category pills */}
227

228	          {/* Status selector */}
229	          <div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold ${statusBg} ${statusColor}`}>
230	            <StatusIcon size={11} />
231	            <select
232	              value={project.status}
233	              onChange={e => updateProject('status', e.target.value)}
234	              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none pr-1"
235	              style={{ color: 'inherit' }}
236	            >
237	              {STATUSES.map(s => <option key={s}>{s}</option>)}
238	            </select>
239

240
241	          {/* Priority selector */}
242	          <div className={`flex items-center px-3 py-1.5 rounded-full text-xs font-semibold ${getPriorityClass(project.priority)}`}>
243	            <select
244	              value={project.priority}
245	              onChange={e => updateProject('priority', e.target.value)}
246	              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none"
247	              style={{ color: 'inherit' }}
248	            >
249	              {PRIORITIES.map(p => <option key={p}>{p}</option>)}
250	            </select>
251

252
253	          {/* Category selector */}
254	          <div className={`flex items-center px-3 py-1.5 rounded-full text-xs font-semibold ${getCategoryClass(project.category)}`}>
255	            <select
256	              value={project.category}
257	              onChange={e => updateProject('category', e.target.value)}
258	              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none"
259	              style={{ color: 'inherit' }}
260	            >
261	              {Object.entries(CATEGORIES).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
262	            </select>
263

264
265	          {/* Task count chip */}
266

267	            <CheckSquare size={11} />
268	            {openTasks} open · {doneTasks} done
269

270

271

272
273	      {/* Body — split layout */}
274

275
276	        {/* ── LEFT: Project info ──────────────────────────────────── */}
277

278

279
280	            <InfoRow icon={AlignLeft} label="Description">
281	              <InlineEdit
282	                value={project.description}
283	                onSave={v => updateProject('description', v)}
284	                multiline
285	                placeholder="Add a description…"
286	                className="leading-relaxed"
287	              />
288	            </InfoRow>
289
290

291
292	            <InfoRow icon={RefreshCw} label="Last Update">
293	              <InlineEdit
294	                value={project.last_update}
295	                onSave={v => updateProject('last_update', v)}
296	                placeholder="What happened last?"
297	              />
298	            </InfoRow>
299
300

301
302	            <InfoRow icon={ChevronRight} label="Next Step">
303	              <InlineEdit
304	                value={project.next_step}
305	                onSave={v => updateProject('next_step', v)}
306	                placeholder="What needs to happen next?"
307	              />
308	            </InfoRow>
309
310

311
312	            {/* Stats */}
313

314	              {[
315	                { label: 'Open',  value: openTasks,        color: 'text-blue-600',  bg: 'bg-blue-50 dark:bg-blue-900/20'  },
316	                { label: 'Done',  value: doneTasks,        color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20'},
317	                { label: 'Notes', value: notes.length,     color: 'text-violet-600',bg: 'bg-violet-50 dark:bg-violet-900/20'},
318	              ].map(({ label, value, color, bg }) => (
319	                <div key={label} className={`${bg} rounded-xl p-3 text-center`}>
320	                  <p className={`text-xl font-bold ${color}`}>{value}

321
{label}

322

323	              ))}
324

325
326	            {project.updated_at && (
327

328	                <Calendar size={9} /> Updated {formatDate(project.updated_at)}
329

330	            )}
331

332

333
334	        {/* ── RIGHT: Tasks / Notes ────────────────────────────────── */}
335

336
337	          {/* Tab bar */}
338

339	            {[
340	              { id: 'tasks', icon: CheckSquare, label: 'Tasks', count: tasks.filter(t => t.status !== 'Done').length },
341	              { id: 'notes', icon: FileText,    label: 'Notes', count: notes.length },
342	            ].map(({ id: tid, icon: Icon, label, count }) => (
343	              <button key={tid} onClick={() => setTab(tid)}
344	                className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-medium transition-all ${
345	                  tab === tid
346	                    ? 'bg-primary text-white shadow-sm'
347	                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-white'
348	                }`}>
349	                <Icon size={13} />{label}
350	                <span className={`text-[10px] px-1.5 py-0.5 rounded-full font-bold ${tab === tid ? 'bg-white/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-500'}`}>
351	                  {count}
352
353	              </button>
354	            ))}
355

356	            {tab === 'tasks' && (
357

358	                <button onClick={openLinkTask} className="btn-secondary flex items-center gap-1 py-1.5 text-xs">
359	                  <Link2 size={12} /> Link
360	                </button>
361	                <button onClick={() => setShowNewTask(true)} className="btn-primary flex items-center gap-1 py-1.5 text-xs">
362	                  <Plus size={12} /> New Task
363	                </button>
364

365	            )}
366	            {tab === 'notes' && (
367	              <button onClick={() => setShowNewNote(!showNewNote)} className="btn-primary flex items-center gap-1 py-1.5 text-xs">
368	                <Plus size={12} /> Add Note
369	              </button>
370	            )}
371

372
373	          {/* Tab content */}
374

375
376	            {/* TASKS */}
377	            {tab === 'tasks' && (
378	              <>
379	                {showNewNote && null}
380	                {tasks.length === 0 ? (
381

382	                    <CheckSquare size={40} className="text-gray-200 dark:text-gray-700" />
383
No tasks yet

384	                    <button onClick={() => setShowNewTask(true)} className="btn-primary text-sm">Add first task</button>
385

386	                ) : (
387

388	                    {tasks.map(t => (
389	                      <TaskCard key={t.id} task={t} onEdit={setEditTask} onDelete={setDeleteTask} onToggleFocus={toggleFocus} />
390	                    ))}
391

392	                )}
393	              </>
394	            )}
395
396	            {/* NOTES */}
397	            {tab === 'notes' && (
398

399	                {showNewNote && (
400	                  <NoteForm projectId={id} onSave={handleSaveNote} onCancel={() => setShowNewNote(false)} />
401	                )}
402	                {notes.length === 0 && !showNewNote ? (
403

404	                    <FileText size={40} className="text-gray-200 dark:text-gray-700" />
405
No notes yet

406	                    <button onClick={() => setShowNewNote(true)} className="btn-primary text-sm">Add first note</button>
407

408	                ) : (
409

410	                    {notes.map(n => (
411

412

413
{n.title}

414	                          <button onClick={() => setDeleteNote(n)}
415	                            className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-red-50 dark:hover:bg-red-900/20 text-gray-400 hover:text-red-500 transition-all shrink-0">
416	                            <Trash2 size={12} />
417	                          </button>
418

419	                        {n.body &&
{n.body}
}
420

421	                          <Calendar size={9} />{formatDate(n.date)}
422

423

424	                    ))}
425

426	                )}
427

428	            )}
429

430

431

432
433	      {/* Modals */}
434	      {(showNewTask || editTask) && (
435	        <TaskModal task={editTask} projects={projects} onSave={handleSaveTask} onClose={() => { setShowNewTask(false); setEditTask(null); }} />
436	      )}
437	      {deleteTask && <ConfirmDialog title="Delete task?" message={`"${deleteTask.title}"`} onConfirm={handleDeleteTask} onCancel={() => setDeleteTask(null)} />}
438	      {deleteNote && <ConfirmDialog title="Delete note?" message={`"${deleteNote.title}"`} onConfirm={handleDeleteNote} onCancel={() => setDeleteNote(null)} />}
439
440	      {/* Link existing task modal */}
441	      {showLinkTask && (
442
 setShowLinkTask(false)}>
443

444
 e.stopPropagation()}>
445

446	              <Link2 size={16} className="text-primary shrink-0" />
447
Link Existing Task

448	              <button onClick={() => setShowLinkTask(false)} className="btn-icon"><X size={16} /></button>
449

450

451

452	                <Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
453	                <input className="input pl-8 py-2 text-sm w-full" placeholder="Search tasks…"
454	                  value={linkSearch} onChange={e => setLinkSearch(e.target.value)} autoFocus />
455

456

457

458	              {allTasks.filter(t =>
459	                !linkSearch || t.title.toLowerCase().includes(linkSearch.toLowerCase()) ||
460	                (t.assignee || '').toLowerCase().includes(linkSearch.toLowerCase())
461	              ).map(task => (
462	                <button key={task.id} onClick={() => linkTask(task)}
463	                  className="flex items-center gap-3 w-full px-5 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left group">
464

465
{task.title}

466

467	                      {task.status}
468	                      {task.assignee && · {task.assignee}}
469

470

471
472	                    <Link2 size={11} /> Link
473
474	                </button>
475	              ))}
476	              {allTasks.filter(t => !linkSearch || t.title.toLowerCase().includes(linkSearch.toLowerCase())).length === 0 && (
477
No unlinked tasks found

478	              )}
479

480

481

482	      )}
483

484	  );
485	}
486

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
  ArrowLeft, Pencil, Plus, Trash2, FileText, CheckSquare,
  Link2, Search, X, CheckCircle2, Clock, Loader2, PauseCircle,
  FolderKanban, AlignLeft, RefreshCw, ChevronRight, Calendar, User
} from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../api';
import TaskCard from './TaskCard';
import TaskModal from './TaskModal';
import ConfirmDialog from './ConfirmDialog';
import { CATEGORIES, STATUSES, PRIORITIES, getCategoryClass, getPriorityClass, getStatusClass, formatDate } from '../utils';

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_BG = {
  ai:        'bg-blue-50 dark:bg-blue-900/20',
  vendor:    'bg-purple-50 dark:bg-purple-900/20',
  gov:       'bg-amber-50 dark:bg-amber-900/20',
  people:    'bg-teal-50 dark:bg-teal-900/20',
  events:    'bg-orange-50 dark:bg-orange-900/20',
  strategic: 'bg-green-50 dark:bg-green-900/20',
  adhoc:     'bg-red-50 dark:bg-red-900/20',
};

const STATUS_META = {
  Open:          { icon: Clock,        color: 'text-gray-500',  bg: 'bg-gray-100 dark:bg-gray-800'         },
  'In Progress': { icon: Loader2,      color: 'text-blue-600',  bg: 'bg-blue-100 dark:bg-blue-900/30'      },
  Waiting:       { icon: PauseCircle,  color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30'    },
  Done:          { icon: CheckCircle2, color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/30'    },
};

function InlineEdit({ value, onSave, multiline = false, placeholder = '', className = '' }) {
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState(value || '');
  useEffect(() => setVal(value || ''), [value]);
  const save = () => { setEditing(false); if (val !== value) onSave(val); };
  if (!editing) return (
     setEditing(true)}
      className={`group cursor-text flex items-start gap-1.5 ${className}`}>

        {val || placeholder}

      <Pencil size={11} className="opacity-0 group-hover:opacity-40 shrink-0 mt-0.5 text-gray-400" />

  );
  return multiline
    ? <textarea autoFocus className="input w-full resize-none text-sm" rows={4} value={val}
        onChange={e => setVal(e.target.value)} onBlur={save} />
    : <input autoFocus className="input w-full text-sm" value={val}
        onChange={e => setVal(e.target.value)} onBlur={save} onKeyDown={e => e.key === 'Enter' && save()} />;
}

function InfoRow({ icon: Icon, label, children }) {
  return (

        <Icon size={11} />{label}

{children}

  );
}

function NoteForm({ projectId, onSave, onCancel }) {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const submit = async (e) => {
    e.preventDefault();
    if (!title.trim()) return;
    await onSave({ title, body, project_id: projectId, date: new Date().toISOString().split('T')[0] });
  };
  return (
    <form onSubmit={submit} className="bg-white dark:bg-gray-900 rounded-2xl border-2 border-dashed border-primary/30 p-4 space-y-3">
      <input className="input font-medium" placeholder="Note title…" value={title} onChange={e => setTitle(e.target.value)} autoFocus required />
      <textarea className="input resize-none text-sm" rows={4} placeholder="Write your note…" value={body} onChange={e => setBody(e.target.value)} />

        <button type="button" onClick={onCancel} className="btn-secondary text-xs py-1.5 px-3">Cancel</button>
        <button type="submit" className="btn-primary text-xs py-1.5 px-3">Save Note</button>

    </form>
  );
}

export default function ProjectDetail() {
  const { id } = useParams();
  const navigate = useNavigate();
  const [project, setProject]       = useState(null);
  const [tasks, setTasks]           = useState([]);
  const [notes, setNotes]           = useState([]);
  const [projects, setProjects]     = useState([]);
  const [tab, setTab]               = useState('tasks');
  const [loading, setLoading]       = useState(true);
  const [editTask, setEditTask]     = useState(null);
  const [deleteTask, setDeleteTask] = useState(null);
  const [deleteNote, setDeleteNote] = useState(null);
  const [showNewTask, setShowNewTask]   = useState(false);
  const [showNewNote, setShowNewNote]   = useState(false);
  const [showLinkTask, setShowLinkTask] = useState(false);
  const [allTasks, setAllTasks]     = useState([]);
  const [linkSearch, setLinkSearch] = useState('');

  const load = async () => {
    try {
      const [p, t, n, pr] = await Promise.all([
        api.get(`/projects/${id}`),
        api.get(`/tasks?project_id=${id}`),
        api.get(`/notes?project_id=${id}`),
        api.get('/projects'),
      ]);
      setProject(p); setTasks(t); setNotes(n); setProjects(pr);
    } catch { navigate('/projects'); }
    finally { setLoading(false); }
  };

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

  const updateProject = async (field, value) => {
    const updated = { ...project, [field]: value };
    setProject(updated);
    await api.put(`/projects/${id}`, updated);
  };

  const handleSaveTask = async (form) => {
    try {
      if (editTask?.id) { await api.put(`/tasks/${editTask.id}`, { ...form, project_id: id }); toast.success('Task updated'); }
      else { await api.post('/tasks', { ...form, project_id: id }); toast.success('Task added'); }
      setEditTask(null); setShowNewTask(false); load();
    } catch (e) { toast.error(e.message); }
  };

  const handleSaveNote = async (form) => {
    try { await api.post('/notes', form); toast.success('Note saved'); setShowNewNote(false); load(); }
    catch (e) { toast.error(e.message); }
  };

  const handleDeleteTask = async () => {
    await api.delete(`/tasks/${deleteTask.id}`); toast.success('Task deleted');
    setDeleteTask(null); load();
  };

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

  const toggleFocus = async (task) => { await api.patch(`/tasks/${task.id}/focus`); load(); };

  const openLinkTask = async () => {
    const all = await api.get('/tasks');
    setAllTasks(all.filter(t => t.project_id != id && t.status !== 'Done'));
    setLinkSearch(''); setShowLinkTask(true);
  };

  const linkTask = async (task) => {
    await api.put(`/tasks/${task.id}`, { ...task, project_id: id });
    toast.success(`Linked to project`);
    setAllTasks(prev => prev.filter(t => t.id !== task.id));
    load();
  };

  if (loading) return (

  );

  if (!project) return null;

  const openTasks  = tasks.filter(t => t.status !== 'Done').length;
  const doneTasks  = tasks.filter(t => t.status === 'Done').length;
  const StatusIcon = STATUS_META[project.status]?.icon || Clock;
  const statusColor = STATUS_META[project.status]?.color || 'text-gray-500';
  const statusBg    = STATUS_META[project.status]?.bg    || 'bg-gray-100';

  return (

      {/* Colored accent bar */}
      <div className={`h-1.5 w-full bg-gradient-to-r shrink-0 ${CATEGORY_ACCENT[project.category] || 'from-gray-400 to-gray-500'}`} />

      {/* Header */}
      <div className={`shrink-0 px-5 pt-4 pb-5 ${CATEGORY_BG[project.category] || 'bg-white dark:bg-gray-900'} border-b border-black/5 dark:border-white/5`}>
        {/* Back + actions */}

          <button onClick={() => navigate('/projects')}
            className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors font-medium">
            <ArrowLeft size={15} /> Projects
          </button>
          <button onClick={() => load()}
            className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-600 transition-colors">
            <RefreshCw size={12} />
          </button>

        {/* Category + title */}

          <div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 bg-white/70 dark:bg-gray-800/70 shadow-sm`}>
            <FolderKanban size={18} className={getCategoryClass(project.category).split(' ').find(c => c.startsWith('text-')) || 'text-gray-500'} />

              <InlineEdit value={project.name} onSave={v => updateProject('name', v)} placeholder="Project name" />

        {/* Status / Priority / Category pills */}

          {/* Status selector */}
          <div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold ${statusBg} ${statusColor}`}>
            <StatusIcon size={11} />
            <select
              value={project.status}
              onChange={e => updateProject('status', e.target.value)}
              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none pr-1"
              style={{ color: 'inherit' }}
            >
              {STATUSES.map(s => <option key={s}>{s}</option>)}
            </select>

          {/* Priority selector */}
          <div className={`flex items-center px-3 py-1.5 rounded-full text-xs font-semibold ${getPriorityClass(project.priority)}`}>
            <select
              value={project.priority}
              onChange={e => updateProject('priority', e.target.value)}
              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none"
              style={{ color: 'inherit' }}
            >
              {PRIORITIES.map(p => <option key={p}>{p}</option>)}
            </select>

          {/* Category selector */}
          <div className={`flex items-center px-3 py-1.5 rounded-full text-xs font-semibold ${getCategoryClass(project.category)}`}>
            <select
              value={project.category}
              onChange={e => updateProject('category', e.target.value)}
              className="bg-transparent border-0 outline-none cursor-pointer text-xs font-semibold appearance-none"
              style={{ color: 'inherit' }}
            >
              {Object.entries(CATEGORIES).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
            </select>

          {/* Task count chip */}

            <CheckSquare size={11} />
            {openTasks} open · {doneTasks} done

      {/* Body — split layout */}

        {/* ── LEFT: Project info ──────────────────────────────────── */}

            <InfoRow icon={AlignLeft} label="Description">
              <InlineEdit
                value={project.description}
                onSave={v => updateProject('description', v)}
                multiline
                placeholder="Add a description…"
                className="leading-relaxed"
              />
            </InfoRow>

            <InfoRow icon={RefreshCw} label="Last Update">
              <InlineEdit
                value={project.last_update}
                onSave={v => updateProject('last_update', v)}
                placeholder="What happened last?"
              />
            </InfoRow>

            <InfoRow icon={ChevronRight} label="Next Step">
              <InlineEdit
                value={project.next_step}
                onSave={v => updateProject('next_step', v)}
                placeholder="What needs to happen next?"
              />
            </InfoRow>

            {/* Stats */}

              {[
                { label: 'Open',  value: openTasks,        color: 'text-blue-600',  bg: 'bg-blue-50 dark:bg-blue-900/20'  },
                { label: 'Done',  value: doneTasks,        color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20'},
                { label: 'Notes', value: notes.length,     color: 'text-violet-600',bg: 'bg-violet-50 dark:bg-violet-900/20'},
              ].map(({ label, value, color, bg }) => (
                <div key={label} className={`${bg} rounded-xl p-3 text-center`}>
                  <p className={`text-xl font-bold ${color}`}>{value}

{label}

              ))}

            {project.updated_at && (

                <Calendar size={9} /> Updated {formatDate(project.updated_at)}

            )}

        {/* ── RIGHT: Tasks / Notes ────────────────────────────────── */}

          {/* Tab bar */}

            {[
              { id: 'tasks', icon: CheckSquare, label: 'Tasks', count: tasks.filter(t => t.status !== 'Done').length },
              { id: 'notes', icon: FileText,    label: 'Notes', count: notes.length },
            ].map(({ id: tid, icon: Icon, label, count }) => (
              <button key={tid} onClick={() => setTab(tid)}
                className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-medium transition-all ${
                  tab === tid
                    ? 'bg-primary text-white shadow-sm'
                    : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-white'
                }`}>
                <Icon size={13} />{label}
                <span className={`text-[10px] px-1.5 py-0.5 rounded-full font-bold ${tab === tid ? 'bg-white/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-500'}`}>
                  {count}

              </button>
            ))}

            {tab === 'tasks' && (

                <button onClick={openLinkTask} className="btn-secondary flex items-center gap-1 py-1.5 text-xs">
                  <Link2 size={12} /> Link
                </button>
                <button onClick={() => setShowNewTask(true)} className="btn-primary flex items-center gap-1 py-1.5 text-xs">
                  <Plus size={12} /> New Task
                </button>

            )}
            {tab === 'notes' && (
              <button onClick={() => setShowNewNote(!showNewNote)} className="btn-primary flex items-center gap-1 py-1.5 text-xs">
                <Plus size={12} /> Add Note
              </button>
            )}

          {/* Tab content */}

            {/* TASKS */}
            {tab === 'tasks' && (
              <>
                {showNewNote && null}
                {tasks.length === 0 ? (

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

No tasks yet

                    <button onClick={() => setShowNewTask(true)} className="btn-primary text-sm">Add first task</button>

                ) : (

                    {tasks.map(t => (
                      <TaskCard key={t.id} task={t} onEdit={setEditTask} onDelete={setDeleteTask} onToggleFocus={toggleFocus} />
                    ))}

                )}
              </>
            )}

            {/* NOTES */}
            {tab === 'notes' && (

                {showNewNote && (
                  <NoteForm projectId={id} onSave={handleSaveNote} onCancel={() => setShowNewNote(false)} />
                )}
                {notes.length === 0 && !showNewNote ? (

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

No notes yet

                    <button onClick={() => setShowNewNote(true)} className="btn-primary text-sm">Add first note</button>

                ) : (

                    {notes.map(n => (

{n.title}

                          <button onClick={() => setDeleteNote(n)}
                            className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-red-50 dark:hover:bg-red-900/20 text-gray-400 hover:text-red-500 transition-all shrink-0">
                            <Trash2 size={12} />
                          </button>

                        {n.body &&
{n.body}
}

                          <Calendar size={9} />{formatDate(n.date)}

                    ))}

                )}

            )}

      {/* Modals */}
      {(showNewTask || editTask) && (
        <TaskModal task={editTask} projects={projects} onSave={handleSaveTask} onClose={() => { setShowNewTask(false); setEditTask(null); }} />
      )}
      {deleteTask && <ConfirmDialog title="Delete task?" message={`"${deleteTask.title}"`} onConfirm={handleDeleteTask} onCancel={() => setDeleteTask(null)} />}
      {deleteNote && <ConfirmDialog title="Delete note?" message={`"${deleteNote.title}"`} onConfirm={handleDeleteNote} onCancel={() => setDeleteNote(null)} />}

      {/* Link existing task modal */}
      {showLinkTask && (

 setShowLinkTask(false)}>

 e.stopPropagation()}>

              <Link2 size={16} className="text-primary shrink-0" />

Link Existing Task

              <button onClick={() => setShowLinkTask(false)} className="btn-icon"><X size={16} /></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 tasks…"
                  value={linkSearch} onChange={e => setLinkSearch(e.target.value)} autoFocus />

              {allTasks.filter(t =>
                !linkSearch || t.title.toLowerCase().includes(linkSearch.toLowerCase()) ||
                (t.assignee || '').toLowerCase().includes(linkSearch.toLowerCase())
              ).map(task => (
                <button key={task.id} onClick={() => linkTask(task)}
                  className="flex items-center gap-3 w-full px-5 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left group">

{task.title}

                      {task.status}
                      {task.assignee && · {task.assignee}}

                    <Link2 size={11} /> Link

                </button>
              ))}
              {allTasks.filter(t => !linkSearch || t.title.toLowerCase().includes(linkSearch.toLowerCase())).length === 0 && (

No unlinked tasks found

              )}

      )}

  );
}


Let me check the server routes for any issues.


1	const express = require('express');
2	const router = express.Router();
3	const db = require('../db');
4
5	// GET /api/tasks
6	router.get('/', (req, res) => {
7	  const { project_id, status, category, priority, search, today_only, follow_up_only } = req.query;
8	  let sql = `
9	    SELECT t.*, p.name as project_name
10	    FROM tasks t
11	    LEFT JOIN projects p ON t.project_id = p.id
12	    WHERE 1=1
13	  `;
14	  const params = [];
15	  if (project_id) { sql += ' AND t.project_id = ?'; params.push(project_id); }
16	  if (status) { sql += ' AND t.status = ?'; params.push(status); }
17	  if (category) { sql += ' AND t.category = ?'; params.push(category); }
18	  if (priority) { sql += ' AND t.priority = ?'; params.push(priority); }
19	  if (today_only === 'true') { sql += " AND t.is_today_focus = 1 AND t.status != 'Done'"; }
20	  if (follow_up_only === 'true') { sql += " AND t.is_follow_up = 1 AND t.status != 'Done'"; }
21	  if (search) {
22	    sql += ' AND (t.title LIKE ? OR t.notes LIKE ? OR t.assignee LIKE ?)';
23	    const q = `%${search}%`;
24	    params.push(q, q, q);
25	  }
26	  sql += ` ORDER BY
27	    CASE t.priority WHEN 'Urgent' THEN 1 WHEN 'High' THEN 2 WHEN 'Medium' THEN 3 ELSE 4 END,
28	    CASE t.status WHEN 'Open' THEN 1 WHEN 'In Progress' THEN 2 WHEN 'Waiting' THEN 3 ELSE 4 END,
29	    t.due_date ASC, t.created_at DESC`;
30	  res.json(db.prepare(sql).all(...params));
31	});
32
33	// GET /api/tasks/stats
34	router.get('/stats', (req, res) => {
35	  const todayFocus = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE is_today_focus = 1 AND status != 'Done'").get().count;
36	  const openTasks = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE status != 'Done'").get().count;
37	  const openFollowUps = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE is_follow_up = 1 AND status != 'Done'").get().count;
38	  const activeProjects = db.prepare("SELECT COUNT(*) as count FROM projects WHERE status != 'Done'").get().count;
39	  res.json({ todayFocus, openTasks, openFollowUps, activeProjects });
40	});
41
42	// GET /api/tasks/:id
43	router.get('/:id', (req, res) => {
44	  const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
45	  if (!task) return res.status(404).json({ error: 'Not found' });
46	  res.json(task);
47	});
48
49	// POST /api/tasks
50	router.post('/', (req, res) => {
51	  const {
52	    title, notes = '', project_id = null, category = 'adhoc', priority = 'Medium',
53	    status = 'Open', assignee = '', due_date = '', is_follow_up = 0, is_today_focus = 0
54	  } = req.body;
55	  if (!title) return res.status(400).json({ error: 'title is required' });
56	  const result = db.prepare(`
57	    INSERT INTO tasks (title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up, is_today_focus)
58	    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
59	  `).run(title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up ? 1 : 0, is_today_focus ? 1 : 0);
60	  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid));
61	});
62
63	// PUT /api/tasks/:id
64	router.put('/:id', (req, res) => {
65	  const {
66	    title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up, is_today_focus
67	  } = req.body;
68	  const prev = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
69	  if (!prev) return res.status(404).json({ error: 'Not found' });
70	  const completedAt = status === 'Done' && prev.status !== 'Done' ? new Date().toISOString() : (status !== 'Done' ? '' : prev.completed_at);
71	  db.prepare(`
72	    UPDATE tasks SET title=?, notes=?, project_id=?, category=?, priority=?, status=?, assignee=?, due_date=?,
73	    is_follow_up=?, is_today_focus=?, completed_at=?, updated_at=datetime('now')
74	    WHERE id=?
75	  `).run(title, notes, project_id, category, priority, status, assignee, due_date,
76	    is_follow_up ? 1 : 0, is_today_focus ? 1 : 0, completedAt, req.params.id);
77	  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id));
78	});
79
80	// DELETE /api/tasks/:id
81	router.delete('/:id', (req, res) => {
82	  db.prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id);
83	  res.json({ success: true });
84	});
85
86	// PATCH /api/tasks/:id/focus — toggle today focus
87	router.patch('/:id/focus', (req, res) => {
88	  const task = db.prepare('SELECT is_today_focus FROM tasks WHERE id = ?').get(req.params.id);
89	  if (!task) return res.status(404).json({ error: 'Not found' });
90	  const newVal = task.is_today_focus ? 0 : 1;
91	  db.prepare("UPDATE tasks SET is_today_focus=?, updated_at=datetime('now') WHERE id=?").run(newVal, req.params.id);
92	  res.json({ is_today_focus: newVal });
93	});
94
95	// PATCH /api/tasks/:id/status — quick status update
96	router.patch('/:id/status', (req, res) => {
97	  const { status } = req.body;
98	  const prev = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
99	  if (!prev) return res.status(404).json({ error: 'Not found' });
100	  const completedAt = status === 'Done' ? new Date().toISOString() : '';
101	  db.prepare("UPDATE tasks SET status=?, completed_at=?, updated_at=datetime('now') WHERE id=?")
102	    .run(status, completedAt, req.params.id);
103	  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id));
104	});
105
106	module.exports = router;
107

const express = require('express');
const router = express.Router();
const db = require('../db');

// GET /api/tasks
router.get('/', (req, res) => {
  const { project_id, status, category, priority, search, today_only, follow_up_only } = req.query;
  let sql = `
    SELECT t.*, p.name as project_name
    FROM tasks t
    LEFT JOIN projects p ON t.project_id = p.id
    WHERE 1=1
  `;
  const params = [];
  if (project_id) { sql += ' AND t.project_id = ?'; params.push(project_id); }
  if (status) { sql += ' AND t.status = ?'; params.push(status); }
  if (category) { sql += ' AND t.category = ?'; params.push(category); }
  if (priority) { sql += ' AND t.priority = ?'; params.push(priority); }
  if (today_only === 'true') { sql += " AND t.is_today_focus = 1 AND t.status != 'Done'"; }
  if (follow_up_only === 'true') { sql += " AND t.is_follow_up = 1 AND t.status != 'Done'"; }
  if (search) {
    sql += ' AND (t.title LIKE ? OR t.notes LIKE ? OR t.assignee LIKE ?)';
    const q = `%${search}%`;
    params.push(q, q, q);
  }
  sql += ` ORDER BY
    CASE t.priority WHEN 'Urgent' THEN 1 WHEN 'High' THEN 2 WHEN 'Medium' THEN 3 ELSE 4 END,
    CASE t.status WHEN 'Open' THEN 1 WHEN 'In Progress' THEN 2 WHEN 'Waiting' THEN 3 ELSE 4 END,
    t.due_date ASC, t.created_at DESC`;
  res.json(db.prepare(sql).all(...params));
});

// GET /api/tasks/stats
router.get('/stats', (req, res) => {
  const todayFocus = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE is_today_focus = 1 AND status != 'Done'").get().count;
  const openTasks = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE status != 'Done'").get().count;
  const openFollowUps = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE is_follow_up = 1 AND status != 'Done'").get().count;
  const activeProjects = db.prepare("SELECT COUNT(*) as count FROM projects WHERE status != 'Done'").get().count;
  res.json({ todayFocus, openTasks, openFollowUps, activeProjects });
});

// GET /api/tasks/:id
router.get('/:id', (req, res) => {
  const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
  if (!task) return res.status(404).json({ error: 'Not found' });
  res.json(task);
});

// POST /api/tasks
router.post('/', (req, res) => {
  const {
    title, notes = '', project_id = null, category = 'adhoc', priority = 'Medium',
    status = 'Open', assignee = '', due_date = '', is_follow_up = 0, is_today_focus = 0
  } = req.body;
  if (!title) return res.status(400).json({ error: 'title is required' });
  const result = db.prepare(`
    INSERT INTO tasks (title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up, is_today_focus)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up ? 1 : 0, is_today_focus ? 1 : 0);
  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid));
});

// PUT /api/tasks/:id
router.put('/:id', (req, res) => {
  const {
    title, notes, project_id, category, priority, status, assignee, due_date, is_follow_up, is_today_focus
  } = req.body;
  const prev = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
  if (!prev) return res.status(404).json({ error: 'Not found' });
  const completedAt = status === 'Done' && prev.status !== 'Done' ? new Date().toISOString() : (status !== 'Done' ? '' : prev.completed_at);
  db.prepare(`
    UPDATE tasks SET title=?, notes=?, project_id=?, category=?, priority=?, status=?, assignee=?, due_date=?,
    is_follow_up=?, is_today_focus=?, completed_at=?, updated_at=datetime('now')
    WHERE id=?
  `).run(title, notes, project_id, category, priority, status, assignee, due_date,
    is_follow_up ? 1 : 0, is_today_focus ? 1 : 0, completedAt, req.params.id);
  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id));
});

// DELETE /api/tasks/:id
router.delete('/:id', (req, res) => {
  db.prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id);
  res.json({ success: true });
});

// PATCH /api/tasks/:id/focus — toggle today focus
router.patch('/:id/focus', (req, res) => {
  const task = db.prepare('SELECT is_today_focus FROM tasks WHERE id = ?').get(req.params.id);
  if (!task) return res.status(404).json({ error: 'Not found' });
  const newVal = task.is_today_focus ? 0 : 1;
  db.prepare("UPDATE tasks SET is_today_focus=?, updated_at=datetime('now') WHERE id=?").run(newVal, req.params.id);
  res.json({ is_today_focus: newVal });
});

// PATCH /api/tasks/:id/status — quick status update
router.patch('/:id/status', (req, res) => {
  const { status } = req.body;
  const prev = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
  if (!prev) return res.status(404).json({ error: 'Not found' });
  const completedAt = status === 'Done' ? new Date().toISOString() : '';
  db.prepare("UPDATE tasks SET status=?, completed_at=?, updated_at=datetime('now') WHERE id=?")
    .run(status, completedAt, req.params.id);
  res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id));
});

module.exports = router;


1	const express = require('express');
2	const router = express.Router();
3	const db = require('../db');
4
5	// GET /api/projects
6	router.get('/', (req, res) => {
7	  const { category, status } = req.query;
8	  let sql = `
9	    SELECT p.*,
10	      (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.status != 'Done') as open_task_count
11	    FROM projects p
12	    WHERE 1=1
13	  `;
14	  const params = [];
15	  if (category) { sql += ' AND p.category = ?'; params.push(category); }
16	  if (status) { sql += ' AND p.status = ?'; params.push(status); }
17	  sql += " ORDER BY CASE p.priority WHEN 'Urgent' THEN 1 WHEN 'High' THEN 2 WHEN 'Medium' THEN 3 ELSE 4 END, p.name";
18	  res.json(db.prepare(sql).all(...params));
19	});
20
21	// GET /api/projects/:id
22	router.get('/:id', (req, res) => {
23	  const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id);
24	  if (!project) return res.status(404).json({ error: 'Not found' });
25	  res.json(project);
26	});
27
28	// POST /api/projects
29	router.post('/', (req, res) => {
30	  const { name, category = 'adhoc', status = 'Open', priority = 'Medium', description = '', last_update = '', next_step = '' } = req.body;
31	  if (!name) return res.status(400).json({ error: 'name is required' });
32	  const result = db.prepare(`
33	    INSERT INTO projects (name, category, status, priority, description, last_update, next_step)
34	    VALUES (?, ?, ?, ?, ?, ?, ?)
35	  `).run(name, category, status, priority, description, last_update, next_step);
36	  res.json(db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid));
37	});
38
39	// PUT /api/projects/:id
40	router.put('/:id', (req, res) => {
41	  const { name, category, status, priority, description, last_update, next_step } = req.body;
42	  db.prepare(`
43	    UPDATE projects SET name=?, category=?, status=?, priority=?, description=?, last_update=?, next_step=?, updated_at=datetime('now')
44	    WHERE id=?
45	  `).run(name, category, status, priority, description, last_update, next_step, req.params.id);
46	  res.json(db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id));
47	});
48
49	// DELETE /api/projects/:id
50	router.delete('/:id', (req, res) => {
51	  db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
52	  res.json({ success: true });
53	});
54
55	module.exports = router;
56

const express = require('express');
const router = express.Router();
const db = require('../db');

// GET /api/projects
router.get('/', (req, res) => {
  const { category, status } = req.query;
  let sql = `
    SELECT p.*,
      (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.status != 'Done') as open_task_count
    FROM projects p
    WHERE 1=1
  `;
  const params = [];
  if (category) { sql += ' AND p.category = ?'; params.push(category); }
  if (status) { sql += ' AND p.status = ?'; params.push(status); }
  sql += " ORDER BY CASE p.priority WHEN 'Urgent' THEN 1 WHEN 'High' THEN 2 WHEN 'Medium' THEN 3 ELSE 4 END, p.name";
  res.json(db.prepare(sql).all(...params));
});

// GET /api/projects/:id
router.get('/:id', (req, res) => {
  const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id);
  if (!project) return res.status(404).json({ error: 'Not found' });
  res.json(project);
});

// POST /api/projects
router.post('/', (req, res) => {
  const { name, category = 'adhoc', status = 'Open', priority = 'Medium', description = '', last_update = '', next_step = '' } = req.body;
  if (!name) return res.status(400).json({ error: 'name is required' });
  const result = db.prepare(`
    INSERT INTO projects (name, category, status, priority, description, last_update, next_step)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).run(name, category, status, priority, description, last_update, next_step);
  res.json(db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid));
});

// PUT /api/projects/:id
router.put('/:id', (req, res) => {
  const { name, category, status, priority, description, last_update, next_step } = req.body;
  db.prepare(`
    UPDATE projects SET name=?, category=?, status=?, priority=?, description=?, last_update=?, next_step=?, updated_at=datetime('now')
    WHERE id=?
  `).run(name, category, status, priority, description, last_update, next_step, req.params.id);
  res.json(db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id));
});

// DELETE /api/projects/:id
router.delete('/:id', (req, res) => {
  db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
  res.json({ success: true });
});

module.exports = router;