Skip to content

Commit 4843fe6

Browse files
committed
feat: add multi task modal and check offs
1 parent c5d9d58 commit 4843fe6

3 files changed

Lines changed: 295 additions & 26 deletions

File tree

src/credit/components/portfolio/TasksBox.tsx

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '../../../shared/compon
22
import { Badge } from '../../../shared/components/ui/badge';
33
import { Button } from '../../../shared/components/ui/button';
44
import { CheckSquare, AlertTriangle, Clock, Eye } from 'lucide-react';
5+
import { TasksModal } from './TasksModal';
6+
import { isTaskCompleted } from '../../utils/taskStorage';
7+
import { useState, useMemo } from 'react';
58

69
export interface Task {
710
id: string;
@@ -56,6 +59,14 @@ const mockTasks: Task[] = [
5659
];
5760

5861
export function TasksBox({ tasks = mockTasks, onTaskClick }: TasksBoxProps) {
62+
const [isModalOpen, setIsModalOpen] = useState(false);
63+
const [refreshKey, setRefreshKey] = useState(0);
64+
65+
// Filter out completed tasks
66+
const activeTasks = useMemo(() => {
67+
return tasks.filter(task => !isTaskCompleted(task.id));
68+
}, [tasks, refreshKey]);
69+
5970
const getTaskIcon = (type: Task['type']) => {
6071
switch (type) {
6172
case 'collateral_risk':
@@ -91,24 +102,37 @@ export function TasksBox({ tasks = mockTasks, onTaskClick }: TasksBoxProps) {
91102
});
92103
};
93104

94-
const highPriorityTasks = tasks.filter(task => task.priority === 'high').length;
105+
const highPriorityTasks = activeTasks.filter(task => task.priority === 'high').length;
106+
107+
const handleTaskCompletionChange = () => {
108+
// Trigger re-render to update filtered tasks
109+
setRefreshKey(prev => prev + 1);
110+
};
95111

96112
return (
97-
<Card>
98-
<CardHeader>
99-
<div className="flex items-center justify-between">
100-
<CardTitle className="flex items-center gap-2 text-lg">
101-
<CheckSquare className="h-5 w-5" />
102-
Tasks
103-
</CardTitle>
104-
<Badge variant="outline" className={highPriorityTasks > 0 ? 'border-red-500/20 text-red-600' : ''}>
105-
{tasks.length} items
106-
</Badge>
107-
</div>
108-
</CardHeader>
109-
<CardContent>
110-
<div className="space-y-3">
111-
{tasks.slice(0, 4).map((task) => {
113+
<>
114+
<Card>
115+
<CardHeader>
116+
<div className="flex items-center justify-between">
117+
<CardTitle className="flex items-center gap-2 text-lg">
118+
<CheckSquare className="h-5 w-5" />
119+
Tasks
120+
</CardTitle>
121+
<Badge variant="outline" className={highPriorityTasks > 0 ? 'border-red-500/20 text-red-600' : ''}>
122+
{activeTasks.length} items
123+
</Badge>
124+
</div>
125+
</CardHeader>
126+
<CardContent>
127+
{activeTasks.length === 0 ? (
128+
<div className="text-center py-8 text-muted-foreground">
129+
<CheckSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
130+
<p className="text-sm">All tasks completed!</p>
131+
</div>
132+
) : (
133+
<>
134+
<div className="space-y-3">
135+
{activeTasks.slice(0, 4).map((task) => {
112136
const Icon = getTaskIcon(task.type);
113137
return (
114138
<div
@@ -144,15 +168,31 @@ export function TasksBox({ tasks = mockTasks, onTaskClick }: TasksBoxProps) {
144168
</div>
145169
);
146170
})}
147-
</div>
148-
{tasks.length > 4 && (
149-
<div className="pt-3 border-t mt-3">
150-
<Button variant="outline" size="sm" className="w-full">
151-
View All Tasks ({tasks.length})
152-
</Button>
153-
</div>
154-
)}
155-
</CardContent>
156-
</Card>
171+
</div>
172+
{activeTasks.length > 4 && (
173+
<div className="pt-3 border-t mt-3">
174+
<Button
175+
variant="outline"
176+
size="sm"
177+
className="w-full"
178+
onClick={() => setIsModalOpen(true)}
179+
>
180+
View All Tasks ({activeTasks.length})
181+
</Button>
182+
</div>
183+
)}
184+
</>
185+
)}
186+
</CardContent>
187+
</Card>
188+
189+
<TasksModal
190+
tasks={activeTasks}
191+
isOpen={isModalOpen}
192+
onClose={() => setIsModalOpen(false)}
193+
onTaskClick={onTaskClick}
194+
onTaskCompletionChange={handleTaskCompletionChange}
195+
/>
196+
</>
157197
);
158198
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../shared/components/ui/dialog';
2+
import { ScrollArea } from '../../../shared/components/ui/scroll-area';
3+
import { Checkbox } from '../../../shared/components/ui/checkbox';
4+
import { Badge } from '../../../shared/components/ui/badge';
5+
import { AlertTriangle, CheckSquare, Clock, Eye } from 'lucide-react';
6+
import { Task } from './TasksBox';
7+
import { isTaskCompleted, markTaskComplete, markTaskIncomplete } from '../../utils/taskStorage';
8+
import { useState, useEffect } from 'react';
9+
10+
interface TasksModalProps {
11+
tasks: Task[];
12+
isOpen: boolean;
13+
onClose: () => void;
14+
onTaskClick?: (task: Task) => void;
15+
onTaskCompletionChange?: () => void;
16+
}
17+
18+
export function TasksModal({ tasks, isOpen, onClose, onTaskClick, onTaskCompletionChange }: TasksModalProps) {
19+
const [completedTasks, setCompletedTasks] = useState<Set<string>>(new Set());
20+
21+
// Load completed tasks from localStorage when modal opens
22+
useEffect(() => {
23+
if (isOpen) {
24+
const completed = tasks
25+
.filter(task => isTaskCompleted(task.id))
26+
.map(task => task.id);
27+
setCompletedTasks(new Set(completed));
28+
}
29+
}, [isOpen, tasks]);
30+
31+
const getTaskIcon = (type: Task['type']) => {
32+
switch (type) {
33+
case 'collateral_risk':
34+
return AlertTriangle;
35+
case 'covenant_action':
36+
return AlertTriangle;
37+
case 'compliance':
38+
return CheckSquare;
39+
case 'review':
40+
return Eye;
41+
default:
42+
return Clock;
43+
}
44+
};
45+
46+
const getPriorityColor = (priority: Task['priority']) => {
47+
switch (priority) {
48+
case 'high':
49+
return 'bg-red-500/10 text-red-600 border-red-500/20';
50+
case 'medium':
51+
return 'bg-orange-500/10 text-orange-600 border-orange-500/20';
52+
case 'low':
53+
return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
54+
default:
55+
return 'bg-gray-500/10 text-gray-600 border-gray-500/20';
56+
}
57+
};
58+
59+
const formatDate = (dateString: string) => {
60+
return new Date(dateString).toLocaleDateString('en-US', {
61+
month: 'short',
62+
day: 'numeric'
63+
});
64+
};
65+
66+
const handleTaskToggle = (taskId: string, checked: boolean) => {
67+
if (checked) {
68+
markTaskComplete(taskId);
69+
setCompletedTasks(prev => new Set([...prev, taskId]));
70+
} else {
71+
markTaskIncomplete(taskId);
72+
setCompletedTasks(prev => {
73+
const next = new Set(prev);
74+
next.delete(taskId);
75+
return next;
76+
});
77+
}
78+
// Notify parent that completion state changed
79+
onTaskCompletionChange?.();
80+
};
81+
82+
const handleTaskItemClick = (task: Task, e: React.MouseEvent) => {
83+
// Don't trigger task click if clicking on the checkbox
84+
if ((e.target as HTMLElement).closest('[role="checkbox"]')) {
85+
return;
86+
}
87+
onTaskClick?.(task);
88+
};
89+
90+
return (
91+
<Dialog open={isOpen} onOpenChange={onClose}>
92+
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
93+
<DialogHeader>
94+
<DialogTitle className="flex items-center gap-2">
95+
<CheckSquare className="h-5 w-5" />
96+
All Tasks
97+
<Badge variant="outline" className="ml-2">
98+
{tasks.length} {tasks.length === 1 ? 'item' : 'items'}
99+
</Badge>
100+
</DialogTitle>
101+
</DialogHeader>
102+
103+
<ScrollArea className="flex-1 pr-4 -mr-4">
104+
<div className="space-y-3 py-2">
105+
{tasks.length === 0 ? (
106+
<div className="text-center py-8 text-muted-foreground">
107+
<CheckSquare className="h-12 w-12 mx-auto mb-3 opacity-50" />
108+
<p>No tasks to display</p>
109+
</div>
110+
) : (
111+
tasks.map((task) => {
112+
const Icon = getTaskIcon(task.type);
113+
const isCompleted = completedTasks.has(task.id);
114+
115+
return (
116+
<div
117+
key={task.id}
118+
className="flex items-start gap-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
119+
onClick={(e) => handleTaskItemClick(task, e)}
120+
>
121+
<Checkbox
122+
checked={isCompleted}
123+
onCheckedChange={(checked) => handleTaskToggle(task.id, checked as boolean)}
124+
className="mt-1"
125+
/>
126+
127+
<div className={`p-1.5 rounded-lg mt-0.5 ${
128+
task.priority === 'high'
129+
? 'bg-red-500/10 text-red-500'
130+
: task.priority === 'medium'
131+
? 'bg-orange-500/10 text-orange-500'
132+
: 'bg-blue-500/10 text-blue-500'
133+
}`}>
134+
<Icon className="h-4 w-4" />
135+
</div>
136+
137+
<div className="flex-1 min-w-0">
138+
<div className="flex items-center gap-2 mb-1">
139+
<p className={`font-medium text-sm ${isCompleted ? 'line-through text-muted-foreground' : ''}`}>
140+
{task.title}
141+
</p>
142+
<Badge className={getPriorityColor(task.priority)} size="sm">
143+
{task.priority}
144+
</Badge>
145+
</div>
146+
<p className={`text-sm ${isCompleted ? 'line-through text-muted-foreground' : 'text-muted-foreground'}`}>
147+
{task.description}
148+
</p>
149+
{task.dueDate && (
150+
<p className="text-xs text-muted-foreground mt-1">
151+
Due {formatDate(task.dueDate)}
152+
</p>
153+
)}
154+
{task.loanId && (
155+
<p className="text-xs text-muted-foreground mt-1">
156+
Loan: {task.loanId}
157+
</p>
158+
)}
159+
</div>
160+
</div>
161+
);
162+
})
163+
)}
164+
</div>
165+
</ScrollArea>
166+
</DialogContent>
167+
</Dialog>
168+
);
169+
}
170+

src/credit/utils/taskStorage.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Task completion storage utilities
3+
* Manages task completion state in localStorage
4+
*/
5+
6+
const STORAGE_KEY = 'arda_completed_tasks';
7+
8+
/**
9+
* Get all completed task IDs from localStorage
10+
*/
11+
export function getCompletedTaskIds(): string[] {
12+
try {
13+
const stored = localStorage.getItem(STORAGE_KEY);
14+
if (!stored) return [];
15+
return JSON.parse(stored);
16+
} catch (error) {
17+
console.error('[taskStorage] Error reading completed tasks:', error);
18+
return [];
19+
}
20+
}
21+
22+
/**
23+
* Mark a task as complete
24+
*/
25+
export function markTaskComplete(taskId: string): void {
26+
try {
27+
const completedTasks = getCompletedTaskIds();
28+
if (!completedTasks.includes(taskId)) {
29+
completedTasks.push(taskId);
30+
localStorage.setItem(STORAGE_KEY, JSON.stringify(completedTasks));
31+
console.log(`[taskStorage] Task ${taskId} marked as complete`);
32+
}
33+
} catch (error) {
34+
console.error('[taskStorage] Error marking task complete:', error);
35+
}
36+
}
37+
38+
/**
39+
* Mark a task as incomplete (remove from completed list)
40+
*/
41+
export function markTaskIncomplete(taskId: string): void {
42+
try {
43+
const completedTasks = getCompletedTaskIds();
44+
const filtered = completedTasks.filter(id => id !== taskId);
45+
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
46+
console.log(`[taskStorage] Task ${taskId} marked as incomplete`);
47+
} catch (error) {
48+
console.error('[taskStorage] Error marking task incomplete:', error);
49+
}
50+
}
51+
52+
/**
53+
* Check if a task is completed
54+
*/
55+
export function isTaskCompleted(taskId: string): boolean {
56+
const completedTasks = getCompletedTaskIds();
57+
return completedTasks.includes(taskId);
58+
}
59+

0 commit comments

Comments
 (0)