pageparse.ai / frontend /src /components /TaskList.tsx
Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
16 kB
import { useState } from 'react';
import type { Task } from '../services/api';
import { PageParseAPI } from '../services/api';
import {
Search, Calendar, AlertTriangle, CheckCircle,
Circle, Trash2, Download, Check, X, Edit2, Copy, Database
} from 'lucide-react';
interface TaskListProps {
tasks: Task[];
onTasksChanged: () => void;
variant?: 'full' | 'compact';
}
export const TaskList = ({ tasks, onTasksChanged, variant = 'full' }: TaskListProps) => {
const [searchQuery, setSearchQuery] = useState('');
const [filterPriority, setFilterPriority] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
const [copiedMD, setCopiedMD] = useState(false);
const [copiedTaskId, setCopiedTaskId] = useState<number | null>(null);
const [editName, setEditName] = useState('');
const [editDueDate, setEditDueDate] = useState('');
const [editPriority, setEditPriority] = useState<'high' | 'medium' | 'low'>('medium');
const [editCategory, setEditCategory] = useState('');
const filteredTasks = tasks.filter(t => {
const q = searchQuery.toLowerCase();
const matchesSearch = t.task.toLowerCase().includes(q) || (t.category?.toLowerCase().includes(q) ?? false);
const matchesPriority = filterPriority === 'all' || t.priority === filterPriority;
const matchesStatus = filterStatus === 'all' || t.status === filterStatus;
return matchesSearch && matchesPriority && matchesStatus;
});
const handleToggleStatus = async (task: Task) => {
const newStatus = task.status === 'todo' ? 'done' : 'todo';
try { await PageParseAPI.updateTask(task.id, { status: newStatus }); onTasksChanged(); }
catch (err) { console.error(err); }
};
const handleDelete = async (id: number) => {
try { await PageParseAPI.deleteTask(id); onTasksChanged(); }
catch (err) { console.error(err); }
};
const startEditing = (task: Task) => {
setEditingTaskId(task.id);
setEditName(task.task);
setEditDueDate(task.due_date || '');
setEditPriority(task.priority || 'medium');
setEditCategory(task.category || '');
};
const saveEditing = async (id: number) => {
try {
await PageParseAPI.updateTask(id, { task: editName, due_date: editDueDate || null, priority: editPriority, category: editCategory || null });
setEditingTaskId(null);
onTasksChanged();
} catch (err) { console.error(err); }
};
const handleCopyMD = async () => {
const md = filteredTasks.map(t => `- [${t.status === 'done' ? 'x' : ' '}] ${t.task}${t.due_date ? ` (due: ${t.due_date})` : ''}`).join('\n');
try { await navigator.clipboard.writeText(md); setCopiedMD(true); setTimeout(() => setCopiedMD(false), 2000); }
catch (err) { console.error(err); }
};
const handleCopyTask = async (task: Task) => {
try { await navigator.clipboard.writeText(task.task); setCopiedTaskId(task.id); setTimeout(() => setCopiedTaskId(null), 2000); }
catch (err) { console.error(err); }
};
const handleExportJSON = () => {
const blob = new Blob([JSON.stringify(filteredTasks, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `pageparse_${new Date().toISOString().split('T')[0]}.json`;
a.click(); URL.revokeObjectURL(url);
};
const handleExportCSV = () => {
const headers = ['ID', 'Task', 'Due Date', 'Priority', 'Category', 'Status', 'Confidence'];
const rows = filteredTasks.map(t => [t.id, `"${t.task.replace(/"/g, '""')}"`, t.due_date || '', t.priority || '', t.category || '', t.status || '', t.ocr_confidence.toFixed(2)]);
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `pageparse_${new Date().toISOString().split('T')[0]}.csv`;
a.click(); URL.revokeObjectURL(url);
};
if (variant === 'compact') {
return (
<div className="section">
<div className="section-header">
<h3><Database size={16} className="text-secondary" /> Recent Records ({filteredTasks.length})</h3>
<div className="flex gap-2 items-center">
<button className="btn btn-sm btn-ghost" onClick={handleCopyMD} disabled={!filteredTasks.length}>{copiedMD ? <Check size={12} /> : <Copy size={12} />}</button>
<button className="btn btn-sm btn-ghost" onClick={handleExportJSON} disabled={!filteredTasks.length}><Download size={12} /> JSON</button>
<button className="btn btn-sm btn-ghost" onClick={handleExportCSV} disabled={!filteredTasks.length}><Download size={12} /> CSV</button>
</div>
</div>
<div className="section-body">
<div className="flex gap-3 mb-3">
<div style={{ position: 'relative', flex: 1 }}>
<Search size={14} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-tertiary)' }} />
<input className="input" style={{ paddingLeft: 32 }} placeholder="Search..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
</div>
<select value={filterPriority} onChange={e => setFilterPriority(e.target.value)} className="select">
<option value="all">All Priority</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="select">
<option value="all">All Status</option>
<option value="todo">To Do</option>
<option value="done">Done</option>
</select>
</div>
<div className="table-wrap">
<table>
<thead>
<tr>
<th style={{ width: 32 }}></th>
<th style={{ width: 80 }}>Type</th>
<th>Task</th>
<th style={{ width: 100 }}>Due</th>
<th style={{ width: 80 }}>Priority</th>
<th style={{ width: 80 }}>Confidence</th>
<th style={{ width: 60 }}></th>
</tr>
</thead>
<tbody>
{filteredTasks.slice(0, 8).map(task => (
<tr key={task.id} className={`${task.status === 'done' ? 'done' : ''} ${(task.ocr_confidence || 0) < 0.6 ? 'low-confidence' : ''}`}>
<td>
<button className="btn-icon" onClick={() => handleToggleStatus(task)}>
{task.status === 'done' ? <CheckCircle size={16} style={{ color: 'var(--success)' }} /> : <Circle size={16} />}
</button>
</td>
<td><span className="text-xs text-tertiary font-semibold" style={{ textTransform: 'uppercase' }}>{(task.type || 'task').replace('_', ' ')}</span></td>
<td>
<div className="flex gap-2 items-center">
<span className="task-text">{task.task}</span>
{(task.ocr_confidence || 0) < 0.6 && <span className="badge badge-medium"><AlertTriangle size={10} /> Low Conf</span>}
</div>
</td>
<td><span className="text-xs text-tertiary">{task.due_date || '—'}</span></td>
<td>{task.priority && <span className={`badge badge-${task.priority}`}>{task.priority}</span>}</td>
<td><span className="text-xs font-mono">{(task.ocr_confidence * 100).toFixed(0)}%</span></td>
<td>
<div className="flex gap-1">
<button className="btn-icon" onClick={() => handleCopyTask(task)}>{copiedTaskId === task.id ? <Check size={14} style={{ color: 'var(--success)' }} /> : <Copy size={14} />}</button>
<button className="btn-icon" onClick={() => handleDelete(task.id)}><Trash2 size={14} style={{ color: 'var(--danger)' }} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{!filteredTasks.length && <div className="history-empty">No records found.</div>}
</div>
</div>
</div>
);
}
return (
<div className="section">
<div className="section-header">
<h3><Database size={16} className="text-secondary" /> All Records ({filteredTasks.length})</h3>
<div className="flex gap-2 items-center">
<button className="btn btn-sm btn-ghost" onClick={handleCopyMD} disabled={!filteredTasks.length}>{copiedMD ? <Check size={12} /> : <Copy size={12} />} Copy MD</button>
<button className="btn btn-sm btn-ghost" onClick={handleExportJSON} disabled={!filteredTasks.length}><Download size={12} /> JSON</button>
<button className="btn btn-sm btn-ghost" onClick={handleExportCSV} disabled={!filteredTasks.length}><Download size={12} /> CSV</button>
</div>
</div>
<div className="section-body">
<div className="flex gap-3 mb-3" style={{ flexWrap: 'wrap' }}>
<div style={{ position: 'relative', flex: 1, minWidth: 200 }}>
<Search size={14} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-tertiary)' }} />
<input className="input" style={{ paddingLeft: 32 }} placeholder="Search tasks or categories..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
</div>
<select value={filterPriority} onChange={e => setFilterPriority(e.target.value)} className="select">
<option value="all">All Priorities</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="select">
<option value="all">All Statuses</option>
<option value="todo">To Do</option>
<option value="done">Completed</option>
</select>
</div>
<div className="table-wrap">
<table>
<thead>
<tr>
<th style={{ width: 32 }}></th>
<th style={{ width: 80 }}>Type</th>
<th>Description</th>
<th style={{ width: 110 }}>Due Date</th>
<th style={{ width: 80 }}>Priority</th>
<th style={{ width: 100 }}>Category</th>
<th style={{ width: 90 }}>Confidence</th>
<th style={{ width: 100 }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredTasks.map(task => {
const isEditing = editingTaskId === task.id;
const isLowConf = (task.ocr_confidence || 0) < 0.6;
return (
<tr key={task.id} className={`${task.status === 'done' ? 'done' : ''} ${isLowConf ? 'low-confidence' : ''}`}>
<td>
<button className="btn-icon" onClick={() => handleToggleStatus(task)}>
{task.status === 'done' ? <CheckCircle size={16} style={{ color: 'var(--success)' }} /> : <Circle size={16} />}
</button>
</td>
<td><span className="text-xs text-tertiary font-semibold" style={{ textTransform: 'uppercase' }}>{(task.type || 'task').replace('_', ' ')}</span></td>
<td>
{isEditing ? (
<input className="input" value={editName} onChange={e => setEditName(e.target.value)} style={{ padding: '4px 8px', fontSize: 13 }} />
) : (
<div className="flex gap-2 items-center">
<span className="task-text">{task.task}</span>
{isLowConf && <span className="badge badge-medium"><AlertTriangle size={10} /> Verify</span>}
</div>
)}
</td>
<td>
{isEditing ? (
<input type="date" className="input" value={editDueDate} onChange={e => setEditDueDate(e.target.value)} style={{ padding: '4px 8px', fontSize: 13 }} />
) : (
<span className="flex items-center gap-1 text-xs text-tertiary"><Calendar size={12} />{task.due_date || '—'}</span>
)}
</td>
<td>
{isEditing ? (
<select value={editPriority} onChange={e => setEditPriority(e.target.value as any)} className="select" style={{ padding: '4px 8px', fontSize: 13 }}>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
) : (
task.priority && <span className={`badge badge-${task.priority}`}>{task.priority}</span>
)}
</td>
<td>
{isEditing ? (
<input className="input" value={editCategory} onChange={e => setEditCategory(e.target.value)} placeholder="e.g. Work" style={{ padding: '4px 8px', fontSize: 13 }} />
) : (
<span className="text-xs text-tertiary">{task.category || '—'}</span>
)}
</td>
<td>
<div className="flex items-center gap-1">
<span style={{ width: 6, height: 6, borderRadius: '50%', background: isLowConf ? 'var(--danger)' : (task.ocr_confidence || 0) < 0.85 ? 'var(--warning)' : 'var(--success)' }} />
<span className="text-xs font-mono">{(task.ocr_confidence * 100).toFixed(0)}%</span>
</div>
</td>
<td>
<div className="flex gap-1" style={{ justifyContent: 'flex-end' }}>
{isEditing ? (
<>
<button className="btn-icon" onClick={() => saveEditing(task.id)}><Check size={14} style={{ color: 'var(--success)' }} /></button>
<button className="btn-icon" onClick={() => setEditingTaskId(null)}><X size={14} style={{ color: 'var(--danger)' }} /></button>
</>
) : (
<>
<button className="btn-icon" onClick={() => handleCopyTask(task)} title="Copy">{copiedTaskId === task.id ? <Check size={14} style={{ color: 'var(--success)' }} /> : <Copy size={14} />}</button>
<button className="btn-icon" onClick={() => startEditing(task)} title="Edit"><Edit2 size={14} /></button>
<button className="btn-icon" onClick={() => handleDelete(task.id)} title="Delete"><Trash2 size={14} style={{ color: 'var(--danger)' }} /></button>
</>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!filteredTasks.length && <div className="history-empty">No records found matching the filters.</div>}
</div>
</div>
</div>
);
};