@fayz-ai/plugin-tasks 0.9.0-next.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/components/TasksTopbarButton.d.ts +1 -1
- package/dist/components/TasksTopbarButton.d.ts.map +1 -1
- package/dist/{TasksContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- package/dist/TasksContext.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/TasksContext.tsx +0 -24
- package/src/components/TaskCard.tsx +0 -81
- package/src/components/TaskDetail.tsx +0 -209
- package/src/components/TaskLabelBadge.tsx +0 -37
- package/src/components/TaskPriorityBadge.tsx +0 -21
- package/src/components/TaskQuickAdd.tsx +0 -42
- package/src/components/TaskStatusGroup.tsx +0 -44
- package/src/components/TasksDrawer.tsx +0 -32
- package/src/components/TasksFilterBar.tsx +0 -57
- package/src/components/TasksGeneralSettings.tsx +0 -41
- package/src/components/TasksPanel.tsx +0 -94
- package/src/components/TasksTopbarButton.tsx +0 -109
- package/src/data/capability.test.ts +0 -43
- package/src/data/mock.ts +0 -143
- package/src/data/supabase.ts +0 -194
- package/src/data/tables.ts +0 -6
- package/src/data/types.ts +0 -16
- package/src/index.ts +0 -202
- package/src/lib/tenant.ts +0 -13
- package/src/locales/en.ts +0 -42
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -42
- package/src/migrations/000_plg_rename.sql +0 -12
- package/src/migrations/001_tasks_base.sql +0 -65
- package/src/migrations/index.ts +0 -89
- package/src/registries.ts +0 -33
- package/src/store.ts +0 -148
- package/src/types.ts +0 -94
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "beta"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.9.
|
|
6
|
+
"version": "0.9.1",
|
|
7
7
|
"description": "Fayz SDK — plugin-tasks plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": false,
|
|
@@ -12,14 +12,12 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"source": "./src/index.ts",
|
|
16
15
|
"types": "./dist/index.d.ts",
|
|
17
16
|
"import": "./dist/index.js"
|
|
18
17
|
}
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"dist"
|
|
22
|
-
"src"
|
|
20
|
+
"dist"
|
|
23
21
|
],
|
|
24
22
|
"peerDependencies": {
|
|
25
23
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -28,9 +26,9 @@
|
|
|
28
26
|
"lucide-react": ">=0.400.0 <1.0.0"
|
|
29
27
|
},
|
|
30
28
|
"dependencies": {
|
|
31
|
-
"@fayz-ai/core": "^0.
|
|
32
|
-
"@fayz-ai/
|
|
33
|
-
"@fayz-ai/ui": "^0.
|
|
29
|
+
"@fayz-ai/core": "^0.10.0",
|
|
30
|
+
"@fayz-ai/admin": "^0.10.0",
|
|
31
|
+
"@fayz-ai/ui": "^0.10.0"
|
|
34
32
|
},
|
|
35
33
|
"devDependencies": {
|
|
36
34
|
"@types/react": "^18.3.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TasksContext.d.ts","sourceRoot":"","sources":["../src/TasksContext.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAM3C,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAID,eAAO,MAAM,oBAAoB;;;;;EAAsB,CAAA;AACvD,eAAO,MAAM,cAAc,2BAAgB,CAAA;AAC3C,eAAO,MAAM,gBAAgB,yBAAkB,CAAA;AAC/C,eAAO,MAAM,aAAa,gDAAe,CAAA"}
|
package/dist/registries.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registries.d.ts","sourceRoot":"","sources":["../src/registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAkBtD,eAAO,MAAM,eAAe,EAAE,iBAAiB,EAc9C,CAAA"}
|
package/src/TasksContext.tsx
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createPluginContext } from '@fayz-ai/saas'
|
|
2
|
-
import type { TasksDataProvider } from './data/types'
|
|
3
|
-
import type { TasksUIState } from './store'
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Config
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
export interface TasksPluginLabels {
|
|
10
|
-
drawerTitle: string
|
|
11
|
-
settingsTitle: string
|
|
12
|
-
quickAddPlaceholder: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ResolvedTasksConfig {
|
|
16
|
-
labels: TasksPluginLabels
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ctx = createPluginContext<ResolvedTasksConfig, TasksDataProvider, TasksUIState>('TasksContextProvider')
|
|
20
|
-
|
|
21
|
-
export const TasksContextProvider = ctx.ContextProvider
|
|
22
|
-
export const useTasksConfig = ctx.useConfig
|
|
23
|
-
export const useTasksProvider = ctx.useProvider
|
|
24
|
-
export const useTasksStore = ctx.useStore
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Calendar, ListChecks } from 'lucide-react'
|
|
3
|
-
import { Checkbox } from '@fayz-ai/ui'
|
|
4
|
-
import { useTasksStore } from '../TasksContext'
|
|
5
|
-
import { TaskPriorityBadge } from './TaskPriorityBadge'
|
|
6
|
-
import { TaskLabelBadges } from './TaskLabelBadge'
|
|
7
|
-
import type { Task } from '../types'
|
|
8
|
-
|
|
9
|
-
function isOverdue(dueDate: string | null): boolean {
|
|
10
|
-
if (!dueDate) return false
|
|
11
|
-
return dueDate < new Date().toISOString().slice(0, 10)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isToday(dueDate: string | null): boolean {
|
|
15
|
-
if (!dueDate) return false
|
|
16
|
-
return dueDate === new Date().toISOString().slice(0, 10)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function formatDueDate(dueDate: string): string {
|
|
20
|
-
const d = new Date(dueDate + 'T00:00:00')
|
|
21
|
-
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function TaskCard({ task, subtaskCount }: { task: Task; subtaskCount?: { done: number; total: number } }) {
|
|
25
|
-
const selectTask = useTasksStore((s) => s.selectTask)
|
|
26
|
-
const updateTask = useTasksStore((s) => s.updateTask)
|
|
27
|
-
|
|
28
|
-
const isDone = task.status === 'done'
|
|
29
|
-
const overdue = !isDone && isOverdue(task.dueDate)
|
|
30
|
-
const dueToday = !isDone && isToday(task.dueDate)
|
|
31
|
-
|
|
32
|
-
const handleCheck = (e: React.MouseEvent) => {
|
|
33
|
-
e.stopPropagation()
|
|
34
|
-
updateTask(task.id, { status: isDone ? 'todo' : 'done' })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div
|
|
39
|
-
onClick={() => selectTask(task.id)}
|
|
40
|
-
className="group flex items-start gap-2.5 rounded-lg border border-transparent px-3 py-2.5 transition-colors hover:bg-muted bg-card shadow-button active:shadow-button-inset hover:border-border cursor-pointer"
|
|
41
|
-
>
|
|
42
|
-
<div className="pt-0.5" onClick={handleCheck}>
|
|
43
|
-
<Checkbox checked={isDone} onChange={() => {}} />
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div className="min-w-0 flex-1 space-y-1">
|
|
47
|
-
<p className={`text-sm leading-snug ${isDone ? 'line-through text-muted-foreground' : ''}`}>
|
|
48
|
-
{task.title}
|
|
49
|
-
</p>
|
|
50
|
-
|
|
51
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
52
|
-
<TaskPriorityBadge priority={task.priority} />
|
|
53
|
-
|
|
54
|
-
{task.dueDate && (
|
|
55
|
-
<span className={`inline-flex items-center gap-1 text-[11px] ${
|
|
56
|
-
overdue ? 'text-destructive font-medium' : dueToday ? 'text-warning font-medium' : 'text-muted-foreground'
|
|
57
|
-
}`}>
|
|
58
|
-
<Calendar className="h-3 w-3" />
|
|
59
|
-
{formatDueDate(task.dueDate)}
|
|
60
|
-
</span>
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
{subtaskCount && subtaskCount.total > 0 && (
|
|
64
|
-
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
65
|
-
<ListChecks className="h-3 w-3" />
|
|
66
|
-
{subtaskCount.done}/{subtaskCount.total}
|
|
67
|
-
</span>
|
|
68
|
-
)}
|
|
69
|
-
|
|
70
|
-
<TaskLabelBadges labelIds={task.labels} max={2} />
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
{task.assignedToName && (
|
|
75
|
-
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary" title={task.assignedToName}>
|
|
76
|
-
{task.assignedToName.split(' ').map((n) => n[0]).join('').slice(0, 2).toUpperCase()}
|
|
77
|
-
</div>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { ArrowLeft, Trash2, Calendar, ChevronDown, Sparkles } from 'lucide-react'
|
|
3
|
-
import { useTasksStore } from '../TasksContext'
|
|
4
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
5
|
-
import { useDelegateToAssistant } from '@fayz-ai/saas'
|
|
6
|
-
import { TaskPriorityBadge } from './TaskPriorityBadge'
|
|
7
|
-
import { TaskLabelBadges } from './TaskLabelBadge'
|
|
8
|
-
import { TaskQuickAdd } from './TaskQuickAdd'
|
|
9
|
-
import { TaskCard } from './TaskCard'
|
|
10
|
-
import type { Task, TaskStatus, TaskPriority } from '../types'
|
|
11
|
-
import { TASK_STATUSES, TASK_PRIORITIES } from '../types'
|
|
12
|
-
|
|
13
|
-
const STATUS_DOT: Record<TaskStatus, string> = {
|
|
14
|
-
todo: 'bg-muted-foreground',
|
|
15
|
-
in_progress: 'bg-info',
|
|
16
|
-
done: 'bg-success',
|
|
17
|
-
cancelled: 'bg-muted-foreground/60',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Same footer format as the Notes tab: time when today, short date otherwise. */
|
|
21
|
-
function formatDateTime(iso: string): string {
|
|
22
|
-
const d = new Date(iso)
|
|
23
|
-
const sameDay = d.toDateString() === new Date().toDateString()
|
|
24
|
-
return sameDay
|
|
25
|
-
? d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
26
|
-
: d.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: '2-digit' })
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function TaskDetail({ taskId }: { taskId: string }) {
|
|
30
|
-
const t = useTranslation()
|
|
31
|
-
const tasks = useTasksStore((s) => s.tasks)
|
|
32
|
-
const selectTask = useTasksStore((s) => s.selectTask)
|
|
33
|
-
const updateTask = useTasksStore((s) => s.updateTask)
|
|
34
|
-
const deleteTask = useTasksStore((s) => s.deleteTask)
|
|
35
|
-
|
|
36
|
-
const task = tasks.find((tt) => tt.id === taskId)
|
|
37
|
-
const delegate = useDelegateToAssistant()
|
|
38
|
-
|
|
39
|
-
const [title, setTitle] = useState('')
|
|
40
|
-
const [description, setDescription] = useState('')
|
|
41
|
-
const [subtasks, setSubtasks] = useState<Task[]>([])
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (task) {
|
|
45
|
-
setTitle(task.title)
|
|
46
|
-
setDescription(task.description ?? '')
|
|
47
|
-
}
|
|
48
|
-
}, [task])
|
|
49
|
-
|
|
50
|
-
useEffect(() => { setSubtasks([]) }, [taskId])
|
|
51
|
-
|
|
52
|
-
const handleTitleBlur = useCallback(() => {
|
|
53
|
-
if (task && title.trim() && title !== task.title) {
|
|
54
|
-
updateTask(task.id, { title: title.trim() })
|
|
55
|
-
}
|
|
56
|
-
}, [task, title, updateTask])
|
|
57
|
-
|
|
58
|
-
const handleDescriptionBlur = useCallback(() => {
|
|
59
|
-
if (task && description !== (task.description ?? '')) {
|
|
60
|
-
updateTask(task.id, { description: description || null })
|
|
61
|
-
}
|
|
62
|
-
}, [task, description, updateTask])
|
|
63
|
-
|
|
64
|
-
const handleDelete = useCallback(() => {
|
|
65
|
-
if (task && confirm(t('tasks.detail.deleteConfirm'))) {
|
|
66
|
-
deleteTask(task.id)
|
|
67
|
-
}
|
|
68
|
-
}, [task, deleteTask, t])
|
|
69
|
-
|
|
70
|
-
if (!task) {
|
|
71
|
-
return (
|
|
72
|
-
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
|
73
|
-
<p className="text-sm">Task not found</p>
|
|
74
|
-
<button type="button" onClick={() => selectTask(null)} className="mt-2 text-xs text-primary hover:underline">
|
|
75
|
-
{t('tasks.detail.back')}
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
// Same frame as the Notes editor: header row with back + icon actions,
|
|
83
|
-
// full-bleed px-4 content, sectioned by hairlines, thin timestamp footer.
|
|
84
|
-
<div className="flex min-h-full flex-col">
|
|
85
|
-
<div className="flex items-center gap-1 border-b border-border/40 px-2 py-1.5">
|
|
86
|
-
<button
|
|
87
|
-
type="button"
|
|
88
|
-
onClick={() => selectTask(null)}
|
|
89
|
-
className="inline-flex items-center gap-1 rounded-lg px-1.5 py-1 text-[11.5px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
90
|
-
>
|
|
91
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
92
|
-
{t('tasks.drawer.title')}
|
|
93
|
-
</button>
|
|
94
|
-
<span className="flex-1" />
|
|
95
|
-
<button
|
|
96
|
-
type="button"
|
|
97
|
-
onClick={handleDelete}
|
|
98
|
-
aria-label={t('tasks.detail.delete')}
|
|
99
|
-
title={t('tasks.detail.delete')}
|
|
100
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
|
101
|
-
>
|
|
102
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<input
|
|
107
|
-
type="text"
|
|
108
|
-
value={title}
|
|
109
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
110
|
-
onBlur={handleTitleBlur}
|
|
111
|
-
onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }}
|
|
112
|
-
className="bg-transparent px-4 pt-3 text-sm font-semibold text-foreground outline-none"
|
|
113
|
-
/>
|
|
114
|
-
|
|
115
|
-
{/* Inline property chips */}
|
|
116
|
-
<div className="flex flex-wrap items-center gap-1.5 px-4 pt-2">
|
|
117
|
-
{/* Status chip */}
|
|
118
|
-
<div className="relative">
|
|
119
|
-
<select
|
|
120
|
-
value={task.status}
|
|
121
|
-
onChange={(e) => updateTask(task.id, { status: e.target.value as TaskStatus })}
|
|
122
|
-
className="appearance-none rounded-full border bg-background pl-5 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors"
|
|
123
|
-
>
|
|
124
|
-
{TASK_STATUSES.map((s) => (
|
|
125
|
-
<option key={s} value={s}>{t(`tasks.status.${s}`)}</option>
|
|
126
|
-
))}
|
|
127
|
-
</select>
|
|
128
|
-
<span className={`absolute left-2 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full ${STATUS_DOT[task.status]}`} />
|
|
129
|
-
<ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" />
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
{/* Priority chip */}
|
|
133
|
-
<div className="relative">
|
|
134
|
-
<select
|
|
135
|
-
value={task.priority}
|
|
136
|
-
onChange={(e) => updateTask(task.id, { priority: e.target.value as TaskPriority })}
|
|
137
|
-
className="appearance-none rounded-full border bg-background pl-2 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors"
|
|
138
|
-
>
|
|
139
|
-
{TASK_PRIORITIES.map((p) => (
|
|
140
|
-
<option key={p} value={p}>{t(`tasks.priority.${p}`)}</option>
|
|
141
|
-
))}
|
|
142
|
-
</select>
|
|
143
|
-
<ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" />
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Due date chip */}
|
|
147
|
-
<div className="relative">
|
|
148
|
-
<input
|
|
149
|
-
type="date"
|
|
150
|
-
value={task.dueDate ?? ''}
|
|
151
|
-
onChange={(e) => updateTask(task.id, { dueDate: e.target.value || null })}
|
|
152
|
-
className="appearance-none rounded-full border bg-background pl-6 pr-2 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors w-[120px]"
|
|
153
|
-
/>
|
|
154
|
-
<Calendar className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" />
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
{/* Labels */}
|
|
158
|
-
{task.labels.length > 0 && <TaskLabelBadges labelIds={task.labels} max={3} />}
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
{/* Description — borderless, fills the middle like the note body */}
|
|
162
|
-
<textarea
|
|
163
|
-
value={description}
|
|
164
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
165
|
-
onBlur={handleDescriptionBlur}
|
|
166
|
-
placeholder={t('tasks.detail.descriptionPlaceholder')}
|
|
167
|
-
className="min-h-[72px] flex-1 resize-none bg-transparent px-4 py-3 text-sm leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
|
|
168
|
-
/>
|
|
169
|
-
|
|
170
|
-
{/* Agentic hand-off — same CTA pattern as the Notes tab */}
|
|
171
|
-
<div className="px-4 pb-2">
|
|
172
|
-
<button
|
|
173
|
-
type="button"
|
|
174
|
-
onClick={() =>
|
|
175
|
-
delegate(
|
|
176
|
-
`${t('tasks.detail.chatWithAi.prompt')}\n\n"${task.title}"${task.description ? `\n${task.description}` : ''}${task.dueDate ? `\n(${t('tasks.detail.dueDate')}: ${task.dueDate})` : ''}`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
className="group flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-primary via-primary/80 to-primary bg-[length:200%_100%] bg-left px-3 py-2 text-[13px] font-semibold text-primary-foreground shadow-sm transition-all duration-500 hover:bg-right hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
180
|
-
>
|
|
181
|
-
<Sparkles className="h-4 w-4 transition-transform duration-300 group-hover:rotate-12" />
|
|
182
|
-
{t('tasks.detail.chatWithAi')}
|
|
183
|
-
</button>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
{/* Subtasks — same section pattern as the note's todos */}
|
|
187
|
-
<div className="border-t border-border/40 px-4 py-2.5">
|
|
188
|
-
<p className="pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
189
|
-
{t('tasks.detail.subtasks')}{subtasks.length > 0 ? ` · ${subtasks.length}` : ''}
|
|
190
|
-
</p>
|
|
191
|
-
<div className="-mx-3">
|
|
192
|
-
<TaskQuickAdd parentId={task.id} />
|
|
193
|
-
</div>
|
|
194
|
-
{subtasks.length > 0 && (
|
|
195
|
-
<div className="divide-y divide-border/40">
|
|
196
|
-
{subtasks.map((st) => (
|
|
197
|
-
<TaskCard key={st.id} task={st} />
|
|
198
|
-
))}
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
{/* Footer: timestamps only — delete lives in the header, like a note */}
|
|
204
|
-
<p className="border-t border-border/40 px-4 py-1.5 text-[10px] text-muted-foreground/60">
|
|
205
|
-
{t('tasks.detail.created')} {formatDateTime(task.createdAt)} · {t('tasks.detail.updated')} {formatDateTime(task.updatedAt)}
|
|
206
|
-
</p>
|
|
207
|
-
</div>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useTasksStore } from '../TasksContext'
|
|
3
|
-
|
|
4
|
-
export function TaskLabelBadge({ labelId }: { labelId: string }) {
|
|
5
|
-
const labels = useTasksStore((s) => s.labels)
|
|
6
|
-
const label = labels.find((l) => l.id === labelId)
|
|
7
|
-
if (!label) return null
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<span
|
|
11
|
-
className="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium leading-none"
|
|
12
|
-
style={{
|
|
13
|
-
backgroundColor: label.color + '20',
|
|
14
|
-
color: label.color,
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
17
|
-
{label.name}
|
|
18
|
-
</span>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function TaskLabelBadges({ labelIds, max = 2 }: { labelIds: string[]; max?: number }) {
|
|
23
|
-
if (labelIds.length === 0) return null
|
|
24
|
-
const visible = labelIds.slice(0, max)
|
|
25
|
-
const overflow = labelIds.length - max
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<span className="inline-flex items-center gap-1">
|
|
29
|
-
{visible.map((id) => (
|
|
30
|
-
<TaskLabelBadge key={id} labelId={id} />
|
|
31
|
-
))}
|
|
32
|
-
{overflow > 0 && (
|
|
33
|
-
<span className="text-[10px] text-muted-foreground">+{overflow}</span>
|
|
34
|
-
)}
|
|
35
|
-
</span>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { AlertTriangle, ArrowUp, ArrowDown, Minus } from 'lucide-react'
|
|
3
|
-
import type { TaskPriority } from '../types'
|
|
4
|
-
|
|
5
|
-
const PRIORITY_CONFIG: Record<TaskPriority, { color: string; bg: string; icon: React.ComponentType<{ className?: string }> }> = {
|
|
6
|
-
low: { color: 'text-muted-foreground', bg: 'bg-muted', icon: ArrowDown },
|
|
7
|
-
medium: { color: 'text-info-soft-foreground', bg: 'bg-info-soft', icon: Minus },
|
|
8
|
-
high: { color: 'text-warning-soft-foreground', bg: 'bg-warning-soft', icon: ArrowUp },
|
|
9
|
-
urgent: { color: 'text-destructive-soft-foreground', bg: 'bg-destructive-soft', icon: AlertTriangle },
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function TaskPriorityBadge({ priority, showLabel }: { priority: TaskPriority; showLabel?: boolean }) {
|
|
13
|
-
const cfg = PRIORITY_CONFIG[priority]
|
|
14
|
-
const Icon = cfg.icon
|
|
15
|
-
return (
|
|
16
|
-
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${cfg.color} ${cfg.bg}`}>
|
|
17
|
-
<Icon className="h-3 w-3" />
|
|
18
|
-
{showLabel && <span className="capitalize">{priority}</span>}
|
|
19
|
-
</span>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react'
|
|
2
|
-
import { Plus } from 'lucide-react'
|
|
3
|
-
import { useTasksStore, useTasksConfig } from '../TasksContext'
|
|
4
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
5
|
-
import type { TaskPriority } from '../types'
|
|
6
|
-
|
|
7
|
-
export function TaskQuickAdd({ parentId }: { parentId?: string | null }) {
|
|
8
|
-
const t = useTranslation()
|
|
9
|
-
const createTask = useTasksStore((s) => s.createTask)
|
|
10
|
-
const [title, setTitle] = useState('')
|
|
11
|
-
const [loading, setLoading] = useState(false)
|
|
12
|
-
|
|
13
|
-
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
14
|
-
e.preventDefault()
|
|
15
|
-
const trimmed = title.trim()
|
|
16
|
-
if (!trimmed || loading) return
|
|
17
|
-
setLoading(true)
|
|
18
|
-
try {
|
|
19
|
-
await createTask({
|
|
20
|
-
title: trimmed,
|
|
21
|
-
parentId: parentId ?? null,
|
|
22
|
-
})
|
|
23
|
-
setTitle('')
|
|
24
|
-
} finally {
|
|
25
|
-
setLoading(false)
|
|
26
|
-
}
|
|
27
|
-
}, [title, loading, createTask, parentId])
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<form onSubmit={handleSubmit} className="flex items-center gap-2 px-3 py-2 border-b">
|
|
31
|
-
<Plus className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
32
|
-
<input
|
|
33
|
-
type="text"
|
|
34
|
-
value={title}
|
|
35
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
36
|
-
placeholder={parentId ? t('tasks.detail.addSubtask') : t('tasks.quickAdd.placeholder')}
|
|
37
|
-
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
|
|
38
|
-
disabled={loading}
|
|
39
|
-
/>
|
|
40
|
-
</form>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
3
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
-
import { TaskCard } from './TaskCard'
|
|
5
|
-
import type { Task, TaskStatus } from '../types'
|
|
6
|
-
|
|
7
|
-
const STATUS_COLORS: Record<TaskStatus, string> = {
|
|
8
|
-
todo: 'bg-muted-foreground',
|
|
9
|
-
in_progress: 'bg-info',
|
|
10
|
-
done: 'bg-success',
|
|
11
|
-
cancelled: 'bg-muted-foreground/60',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function TaskStatusGroup({ status, tasks }: { status: TaskStatus; tasks: Task[] }) {
|
|
15
|
-
const t = useTranslation()
|
|
16
|
-
const [collapsed, setCollapsed] = useState(status === 'cancelled')
|
|
17
|
-
|
|
18
|
-
if (tasks.length === 0) return null
|
|
19
|
-
|
|
20
|
-
const Chevron = collapsed ? ChevronRight : ChevronDown
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<div className="space-y-0.5">
|
|
24
|
-
<button
|
|
25
|
-
type="button"
|
|
26
|
-
onClick={() => setCollapsed(!collapsed)}
|
|
27
|
-
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
|
|
28
|
-
>
|
|
29
|
-
<span className={`h-2 w-2 rounded-full ${STATUS_COLORS[status]}`} />
|
|
30
|
-
<span>{t(`tasks.status.${status}`)}</span>
|
|
31
|
-
<span className="text-[10px] font-normal">({tasks.length})</span>
|
|
32
|
-
<Chevron className="ml-auto h-3.5 w-3.5" />
|
|
33
|
-
</button>
|
|
34
|
-
|
|
35
|
-
{!collapsed && (
|
|
36
|
-
<div>
|
|
37
|
-
{tasks.map((task) => (
|
|
38
|
-
<TaskCard key={task.id} task={task} />
|
|
39
|
-
))}
|
|
40
|
-
</div>
|
|
41
|
-
)}
|
|
42
|
-
</div>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Sheet, SheetContent, SheetHeader, SheetBody, SheetTitle, SheetDescription,
|
|
4
|
-
} from '@fayz-ai/ui'
|
|
5
|
-
import { useTasksStore } from '../TasksContext'
|
|
6
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
7
|
-
import { TasksPanel, useTasksSummaryLine } from './TasksPanel'
|
|
8
|
-
|
|
9
|
-
/** Tasks as a sheet — the fallback where the shell has no right rail. */
|
|
10
|
-
export function TasksDrawer() {
|
|
11
|
-
const t = useTranslation()
|
|
12
|
-
const drawerOpen = useTasksStore((s) => s.drawerOpen)
|
|
13
|
-
const closeDrawer = useTasksStore((s) => s.closeDrawer)
|
|
14
|
-
const summaryLine = useTasksSummaryLine()
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<Sheet open={drawerOpen} onOpenChange={(v) => { if (!v) closeDrawer() }}>
|
|
18
|
-
<SheetContent width="max-w-lg" overlay="dim">
|
|
19
|
-
<SheetHeader>
|
|
20
|
-
<SheetTitle>{t('tasks.drawer.title')}</SheetTitle>
|
|
21
|
-
<SheetDescription>
|
|
22
|
-
{summaryLine || t('tasks.drawer.subtitle')}
|
|
23
|
-
</SheetDescription>
|
|
24
|
-
</SheetHeader>
|
|
25
|
-
|
|
26
|
-
<SheetBody className="flex flex-col px-0 py-0">
|
|
27
|
-
<TasksPanel />
|
|
28
|
-
</SheetBody>
|
|
29
|
-
</SheetContent>
|
|
30
|
-
</Sheet>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useTasksStore } from '../TasksContext'
|
|
3
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
-
import type { TaskStatus } from '../types'
|
|
5
|
-
|
|
6
|
-
const FILTERS: Array<{ key: TaskStatus | 'all'; labelKey: string }> = [
|
|
7
|
-
{ key: 'all', labelKey: 'tasks.filter.all' },
|
|
8
|
-
{ key: 'todo', labelKey: 'tasks.filter.todo' },
|
|
9
|
-
{ key: 'in_progress', labelKey: 'tasks.filter.inProgress' },
|
|
10
|
-
{ key: 'done', labelKey: 'tasks.filter.done' },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
export function TasksFilterBar() {
|
|
14
|
-
const t = useTranslation()
|
|
15
|
-
const activeFilter = useTasksStore((s) => s.activeFilter)
|
|
16
|
-
const setFilter = useTasksStore((s) => s.setFilter)
|
|
17
|
-
const summary = useTasksStore((s) => s.summary)
|
|
18
|
-
|
|
19
|
-
const getCount = (key: TaskStatus | 'all'): number | undefined => {
|
|
20
|
-
if (!summary) return undefined
|
|
21
|
-
switch (key) {
|
|
22
|
-
case 'all': return summary.total
|
|
23
|
-
case 'todo': return summary.todo
|
|
24
|
-
case 'in_progress': return summary.inProgress
|
|
25
|
-
case 'done': return summary.done
|
|
26
|
-
default: return undefined
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<div className="flex items-center gap-1 px-3 py-2 border-b overflow-x-auto">
|
|
32
|
-
{FILTERS.map(({ key, labelKey }) => {
|
|
33
|
-
const active = activeFilter === key
|
|
34
|
-
const count = getCount(key)
|
|
35
|
-
return (
|
|
36
|
-
<button
|
|
37
|
-
key={key}
|
|
38
|
-
type="button"
|
|
39
|
-
onClick={() => setFilter(key)}
|
|
40
|
-
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors whitespace-nowrap ${
|
|
41
|
-
active
|
|
42
|
-
? 'bg-primary text-primary-foreground'
|
|
43
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
44
|
-
}`}
|
|
45
|
-
>
|
|
46
|
-
{t(labelKey)}
|
|
47
|
-
{count != null && (
|
|
48
|
-
<span className={`text-[10px] ${active ? 'text-primary-foreground/80' : 'text-muted-foreground'}`}>
|
|
49
|
-
{count}
|
|
50
|
-
</span>
|
|
51
|
-
)}
|
|
52
|
-
</button>
|
|
53
|
-
)
|
|
54
|
-
})}
|
|
55
|
-
</div>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
3
|
-
import { useTenantPluginSettings } from '@fayz-ai/saas'
|
|
4
|
-
|
|
5
|
-
// Per-tenant defaults = the value that used to be the hardcoded `selected` option.
|
|
6
|
-
// Persisted + re-hydrated per tenant; behavioural consumption is a follow-up.
|
|
7
|
-
const TASKS_DEFAULTS = {
|
|
8
|
-
defaultPriority: 'medium',
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function TasksGeneralSettings() {
|
|
12
|
-
const t = useTranslation()
|
|
13
|
-
const s = useTenantPluginSettings('tasks', TASKS_DEFAULTS)
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<div className="space-y-6">
|
|
17
|
-
<div>
|
|
18
|
-
<h3 className="text-sm font-semibold mb-1">{t('tasks.settings.general')}</h3>
|
|
19
|
-
<p className="text-xs text-muted-foreground mb-3">
|
|
20
|
-
{t('tasks.drawer.subtitle')}
|
|
21
|
-
</p>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div className="space-y-4">
|
|
25
|
-
<div className="space-y-1.5">
|
|
26
|
-
<label className="text-sm font-medium">{t('tasks.settings.defaultPriority')}</label>
|
|
27
|
-
<select
|
|
28
|
-
className="w-full max-w-xs rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm"
|
|
29
|
-
value={s.get('defaultPriority')}
|
|
30
|
-
onChange={(e) => s.set('defaultPriority', e.target.value)}
|
|
31
|
-
>
|
|
32
|
-
<option value="low">{t('tasks.priority.low')}</option>
|
|
33
|
-
<option value="medium">{t('tasks.priority.medium')}</option>
|
|
34
|
-
<option value="high">{t('tasks.priority.high')}</option>
|
|
35
|
-
<option value="urgent">{t('tasks.priority.urgent')}</option>
|
|
36
|
-
</select>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|