@fayz-ai/plugin-tasks 0.1.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/dist/TasksContext.d.ts +20 -0
- package/dist/TasksContext.d.ts.map +1 -0
- package/dist/components/TaskCard.d.ts +10 -0
- package/dist/components/TaskCard.d.ts.map +1 -0
- package/dist/components/TaskDetail.d.ts +5 -0
- package/dist/components/TaskDetail.d.ts.map +1 -0
- package/dist/components/TaskLabelBadge.d.ts +9 -0
- package/dist/components/TaskLabelBadge.d.ts.map +1 -0
- package/dist/components/TaskPriorityBadge.d.ts +7 -0
- package/dist/components/TaskPriorityBadge.d.ts.map +1 -0
- package/dist/components/TaskQuickAdd.d.ts +5 -0
- package/dist/components/TaskQuickAdd.d.ts.map +1 -0
- package/dist/components/TaskStatusGroup.d.ts +7 -0
- package/dist/components/TaskStatusGroup.d.ts.map +1 -0
- package/dist/components/TasksDrawer.d.ts +3 -0
- package/dist/components/TasksDrawer.d.ts.map +1 -0
- package/dist/components/TasksFilterBar.d.ts +3 -0
- package/dist/components/TasksFilterBar.d.ts.map +1 -0
- package/dist/components/TasksGeneralSettings.d.ts +3 -0
- package/dist/components/TasksGeneralSettings.d.ts.map +1 -0
- package/dist/components/TasksTopbarButton.d.ts +15 -0
- package/dist/components/TasksTopbarButton.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +13 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +1108 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1102 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +27 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/TasksContext.tsx +24 -0
- package/src/components/TaskCard.tsx +81 -0
- package/src/components/TaskDetail.tsx +184 -0
- package/src/components/TaskLabelBadge.tsx +37 -0
- package/src/components/TaskPriorityBadge.tsx +21 -0
- package/src/components/TaskQuickAdd.tsx +42 -0
- package/src/components/TaskStatusGroup.tsx +44 -0
- package/src/components/TasksDrawer.tsx +94 -0
- package/src/components/TasksFilterBar.tsx +57 -0
- package/src/components/TasksGeneralSettings.tsx +29 -0
- package/src/components/TasksTopbarButton.tsx +64 -0
- package/src/data/capability.test.ts +43 -0
- package/src/data/mock.ts +143 -0
- package/src/data/supabase.ts +193 -0
- package/src/data/types.ts +16 -0
- package/src/index.ts +217 -0
- package/src/lib/tenant.ts +13 -0
- package/src/locales/en.ts +40 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +40 -0
- package/src/migrations/001_tasks_base.sql +57 -0
- package/src/registries.ts +32 -0
- package/src/store.ts +148 -0
- package/src/types.ts +94 -0
package/src/data/mock.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { TasksDataProvider } from './types'
|
|
2
|
+
import type {
|
|
3
|
+
Task, TaskLabel, CreateTaskInput, UpdateTaskInput,
|
|
4
|
+
TaskQuery, TasksSummary, TaskStatus,
|
|
5
|
+
} from '../types'
|
|
6
|
+
|
|
7
|
+
function uid(): string { return crypto.randomUUID?.() ?? String(Date.now()) }
|
|
8
|
+
function now(): string { return new Date().toISOString() }
|
|
9
|
+
function today(): string { return new Date().toISOString().slice(0, 10) }
|
|
10
|
+
|
|
11
|
+
const SEED_LABELS: TaskLabel[] = [
|
|
12
|
+
{ id: 'lbl-bug', name: 'Bug', color: '#ef4444', isActive: true, tenantId: 'mock', createdAt: now() },
|
|
13
|
+
{ id: 'lbl-feature', name: 'Feature', color: '#3b82f6', isActive: true, tenantId: 'mock', createdAt: now() },
|
|
14
|
+
{ id: 'lbl-urgent', name: 'Urgent', color: '#f59e0b', isActive: true, tenantId: 'mock', createdAt: now() },
|
|
15
|
+
{ id: 'lbl-personal', name: 'Personal', color: '#8b5cf6', isActive: true, tenantId: 'mock', createdAt: now() },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
function makeSeedTasks(): Task[] {
|
|
19
|
+
const base = {
|
|
20
|
+
tenantId: 'mock',
|
|
21
|
+
description: null,
|
|
22
|
+
assignedToId: null,
|
|
23
|
+
assignedToName: null,
|
|
24
|
+
parentId: null,
|
|
25
|
+
labels: [],
|
|
26
|
+
createdById: null,
|
|
27
|
+
createdByName: null,
|
|
28
|
+
createdAt: now(),
|
|
29
|
+
updatedAt: now(),
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
{ ...base, id: uid(), title: 'Setup payment gateway', status: 'todo' as const, priority: 'high' as const, dueDate: today(), position: 0 },
|
|
33
|
+
{ ...base, id: uid(), title: 'Update menu photos', status: 'todo' as const, priority: 'medium' as const, dueDate: null, position: 1 },
|
|
34
|
+
{ ...base, id: uid(), title: 'Review monthly report', status: 'in_progress' as const, priority: 'medium' as const, dueDate: null, position: 0 },
|
|
35
|
+
{ ...base, id: uid(), title: 'Fix login bug', status: 'in_progress' as const, priority: 'urgent' as const, dueDate: today(), position: 1, labels: ['lbl-bug'] },
|
|
36
|
+
{ ...base, id: uid(), title: 'Onboard new employee', status: 'done' as const, priority: 'low' as const, dueDate: null, position: 0 },
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchesQuery(task: Task, query: TaskQuery): boolean {
|
|
41
|
+
if (query.status) {
|
|
42
|
+
const statuses = Array.isArray(query.status) ? query.status : [query.status]
|
|
43
|
+
if (!statuses.includes(task.status)) return false
|
|
44
|
+
}
|
|
45
|
+
if (query.priority) {
|
|
46
|
+
const priorities = Array.isArray(query.priority) ? query.priority : [query.priority]
|
|
47
|
+
if (!priorities.includes(task.priority)) return false
|
|
48
|
+
}
|
|
49
|
+
if (query.assignedToId && task.assignedToId !== query.assignedToId) return false
|
|
50
|
+
if (query.parentId !== undefined) {
|
|
51
|
+
if (query.parentId === null && task.parentId !== null) return false
|
|
52
|
+
if (query.parentId !== null && task.parentId !== query.parentId) return false
|
|
53
|
+
}
|
|
54
|
+
if (query.search) {
|
|
55
|
+
const s = query.search.toLowerCase()
|
|
56
|
+
if (!task.title.toLowerCase().includes(s)) return false
|
|
57
|
+
}
|
|
58
|
+
if (query.overdue && task.dueDate) {
|
|
59
|
+
if (task.dueDate >= today()) return false
|
|
60
|
+
}
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createMockTasksProvider(): TasksDataProvider {
|
|
65
|
+
let tasks = makeSeedTasks()
|
|
66
|
+
const labels = [...SEED_LABELS]
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
async getTasks(query) {
|
|
70
|
+
return tasks.filter((t) => matchesQuery(t, query)).sort((a, b) => a.position - b.position)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async getTaskById(id) {
|
|
74
|
+
return tasks.find((t) => t.id === id) ?? null
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async getSubtasks(parentId) {
|
|
78
|
+
return tasks.filter((t) => t.parentId === parentId).sort((a, b) => a.position - b.position)
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async createTask(input: CreateTaskInput) {
|
|
82
|
+
const statusTasks = tasks.filter((t) => t.status === (input.status ?? 'todo') && t.parentId === (input.parentId ?? null))
|
|
83
|
+
const maxPos = statusTasks.reduce((max, t) => Math.max(max, t.position), -1)
|
|
84
|
+
const task: Task = {
|
|
85
|
+
id: uid(),
|
|
86
|
+
title: input.title,
|
|
87
|
+
description: input.description ?? null,
|
|
88
|
+
status: input.status ?? 'todo',
|
|
89
|
+
priority: input.priority ?? 'medium',
|
|
90
|
+
dueDate: input.dueDate ?? null,
|
|
91
|
+
assignedToId: input.assignedToId ?? null,
|
|
92
|
+
assignedToName: null,
|
|
93
|
+
parentId: input.parentId ?? null,
|
|
94
|
+
labels: input.labels ?? [],
|
|
95
|
+
position: maxPos + 1,
|
|
96
|
+
createdById: null,
|
|
97
|
+
createdByName: null,
|
|
98
|
+
tenantId: 'mock',
|
|
99
|
+
createdAt: now(),
|
|
100
|
+
updatedAt: now(),
|
|
101
|
+
}
|
|
102
|
+
tasks.push(task)
|
|
103
|
+
return task
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async updateTask(id, input: UpdateTaskInput) {
|
|
107
|
+
const idx = tasks.findIndex((t) => t.id === id)
|
|
108
|
+
if (idx === -1) throw new Error('Task not found')
|
|
109
|
+
tasks[idx] = { ...tasks[idx], ...input, updatedAt: now() } as Task
|
|
110
|
+
return tasks[idx]
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async deleteTask(id) {
|
|
114
|
+
tasks = tasks.filter((t) => t.id !== id && t.parentId !== id)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async reorderTask(id, newPosition, status) {
|
|
118
|
+
const task = tasks.find((t) => t.id === id)
|
|
119
|
+
if (!task) return
|
|
120
|
+
task.status = status
|
|
121
|
+
task.position = newPosition
|
|
122
|
+
task.updatedAt = now()
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async getLabels() {
|
|
126
|
+
return labels.filter((l) => l.isActive)
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async getSummary() {
|
|
130
|
+
const topLevel = tasks.filter((t) => t.parentId === null)
|
|
131
|
+
const d = today()
|
|
132
|
+
return {
|
|
133
|
+
total: topLevel.length,
|
|
134
|
+
todo: topLevel.filter((t) => t.status === 'todo').length,
|
|
135
|
+
inProgress: topLevel.filter((t) => t.status === 'in_progress').length,
|
|
136
|
+
done: topLevel.filter((t) => t.status === 'done').length,
|
|
137
|
+
overdue: topLevel.filter((t) => t.dueDate != null && t.dueDate < d && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
138
|
+
dueToday: topLevel.filter((t) => t.dueDate === d && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
139
|
+
highPriority: topLevel.filter((t) => (t.priority === 'high' || t.priority === 'urgent') && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { TasksDataProvider } from './types'
|
|
2
|
+
import type { Task, TaskLabel, TasksSummary, TaskStatus } from '../types'
|
|
3
|
+
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
4
|
+
import { getTasksTenantId } from '../lib/tenant'
|
|
5
|
+
|
|
6
|
+
function getTenantId(): string | undefined {
|
|
7
|
+
// Local override wins; else use the app's active tenant so writes pass RLS.
|
|
8
|
+
return getTasksTenantId() ?? getActiveTenantId()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getClient() {
|
|
12
|
+
const supabase = getSupabaseClientOptional() as any
|
|
13
|
+
if (!supabase) throw new Error('Supabase not initialized')
|
|
14
|
+
return supabase
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
18
|
+
const result: Record<string, unknown> = {}
|
|
19
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
20
|
+
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value
|
|
21
|
+
}
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toTask(row: Record<string, unknown>): Task {
|
|
26
|
+
const t = snakeToCamel(row) as unknown as Task
|
|
27
|
+
// Ensure labels is always an array
|
|
28
|
+
if (!Array.isArray(t.labels)) t.labels = []
|
|
29
|
+
return t
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createSupabaseTasksProvider(): TasksDataProvider {
|
|
33
|
+
return {
|
|
34
|
+
async getTasks(query) {
|
|
35
|
+
const tenantId = getTenantId()
|
|
36
|
+
if (!tenantId) return []
|
|
37
|
+
let q = getClient()
|
|
38
|
+
.from('tsk_tasks')
|
|
39
|
+
.select('*')
|
|
40
|
+
.eq('tenant_id', tenantId)
|
|
41
|
+
|
|
42
|
+
if (query.parentId === null) {
|
|
43
|
+
q = q.is('parent_id', null)
|
|
44
|
+
} else if (query.parentId) {
|
|
45
|
+
q = q.eq('parent_id', query.parentId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (query.status) {
|
|
49
|
+
const statuses = Array.isArray(query.status) ? query.status : [query.status]
|
|
50
|
+
q = q.in('status', statuses)
|
|
51
|
+
}
|
|
52
|
+
if (query.priority) {
|
|
53
|
+
const priorities = Array.isArray(query.priority) ? query.priority : [query.priority]
|
|
54
|
+
q = q.in('priority', priorities)
|
|
55
|
+
}
|
|
56
|
+
if (query.assignedToId) {
|
|
57
|
+
q = q.eq('assigned_to_id', query.assignedToId)
|
|
58
|
+
}
|
|
59
|
+
if (query.search) {
|
|
60
|
+
q = q.ilike('title', `%${query.search}%`)
|
|
61
|
+
}
|
|
62
|
+
if (query.overdue) {
|
|
63
|
+
q = q.lt('due_date', new Date().toISOString().slice(0, 10))
|
|
64
|
+
.not('status', 'in', '("done","cancelled")')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
q = q.order('position', { ascending: true }).order('created_at', { ascending: false })
|
|
68
|
+
|
|
69
|
+
const { data } = await q
|
|
70
|
+
return (data ?? []).map(toTask)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async getTaskById(id) {
|
|
74
|
+
const { data } = await getClient()
|
|
75
|
+
.from('tsk_tasks')
|
|
76
|
+
.select('*')
|
|
77
|
+
.eq('id', id)
|
|
78
|
+
.maybeSingle()
|
|
79
|
+
return data ? toTask(data) : null
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async getSubtasks(parentId) {
|
|
83
|
+
const { data } = await getClient()
|
|
84
|
+
.from('tsk_tasks')
|
|
85
|
+
.select('*')
|
|
86
|
+
.eq('parent_id', parentId)
|
|
87
|
+
.order('position', { ascending: true })
|
|
88
|
+
return (data ?? []).map(toTask)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async createTask(input) {
|
|
92
|
+
const tenantId = getTenantId()
|
|
93
|
+
if (!tenantId) throw new Error('No active tenant')
|
|
94
|
+
|
|
95
|
+
// Get max position for the status group
|
|
96
|
+
const { data: maxData } = await getClient()
|
|
97
|
+
.from('tsk_tasks')
|
|
98
|
+
.select('position')
|
|
99
|
+
.eq('tenant_id', tenantId)
|
|
100
|
+
.eq('status', input.status ?? 'todo')
|
|
101
|
+
.is('parent_id', input.parentId ?? null)
|
|
102
|
+
.order('position', { ascending: false })
|
|
103
|
+
.limit(1)
|
|
104
|
+
const maxPos = maxData?.[0]?.position ?? -1
|
|
105
|
+
|
|
106
|
+
const { data, error } = await getClient()
|
|
107
|
+
.from('tsk_tasks')
|
|
108
|
+
.insert({
|
|
109
|
+
tenant_id: tenantId,
|
|
110
|
+
title: input.title,
|
|
111
|
+
description: input.description ?? null,
|
|
112
|
+
status: input.status ?? 'todo',
|
|
113
|
+
priority: input.priority ?? 'medium',
|
|
114
|
+
due_date: input.dueDate ?? null,
|
|
115
|
+
assigned_to_id: input.assignedToId ?? null,
|
|
116
|
+
parent_id: input.parentId ?? null,
|
|
117
|
+
labels: input.labels ?? [],
|
|
118
|
+
position: maxPos + 1,
|
|
119
|
+
})
|
|
120
|
+
.select('*')
|
|
121
|
+
.single()
|
|
122
|
+
if (error) throw error
|
|
123
|
+
return toTask(data)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async updateTask(id, input) {
|
|
127
|
+
const payload: Record<string, unknown> = { updated_at: new Date().toISOString() }
|
|
128
|
+
if (input.title !== undefined) payload.title = input.title
|
|
129
|
+
if (input.description !== undefined) payload.description = input.description
|
|
130
|
+
if (input.status !== undefined) payload.status = input.status
|
|
131
|
+
if (input.priority !== undefined) payload.priority = input.priority
|
|
132
|
+
if (input.dueDate !== undefined) payload.due_date = input.dueDate
|
|
133
|
+
if (input.assignedToId !== undefined) payload.assigned_to_id = input.assignedToId
|
|
134
|
+
if (input.labels !== undefined) payload.labels = input.labels
|
|
135
|
+
if (input.position !== undefined) payload.position = input.position
|
|
136
|
+
|
|
137
|
+
const { data, error } = await getClient()
|
|
138
|
+
.from('tsk_tasks')
|
|
139
|
+
.update(payload)
|
|
140
|
+
.eq('id', id)
|
|
141
|
+
.select('*')
|
|
142
|
+
.single()
|
|
143
|
+
if (error) throw error
|
|
144
|
+
return toTask(data)
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async deleteTask(id) {
|
|
148
|
+
await getClient().from('tsk_tasks').delete().eq('id', id)
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async reorderTask(id, newPosition, status) {
|
|
152
|
+
await getClient()
|
|
153
|
+
.from('tsk_tasks')
|
|
154
|
+
.update({ position: newPosition, status, updated_at: new Date().toISOString() })
|
|
155
|
+
.eq('id', id)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async getLabels() {
|
|
159
|
+
const tenantId = getTenantId()
|
|
160
|
+
if (!tenantId) return []
|
|
161
|
+
const { data } = await getClient()
|
|
162
|
+
.from('tsk_labels')
|
|
163
|
+
.select('*')
|
|
164
|
+
.eq('tenant_id', tenantId)
|
|
165
|
+
.eq('is_active', true)
|
|
166
|
+
.order('name')
|
|
167
|
+
return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as TaskLabel)
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
async getSummary() {
|
|
171
|
+
const tenantId = getTenantId()
|
|
172
|
+
if (!tenantId) return { total: 0, todo: 0, inProgress: 0, done: 0, overdue: 0, dueToday: 0, highPriority: 0 }
|
|
173
|
+
|
|
174
|
+
const { data } = await getClient()
|
|
175
|
+
.from('tsk_tasks')
|
|
176
|
+
.select('status, priority, due_date')
|
|
177
|
+
.eq('tenant_id', tenantId)
|
|
178
|
+
.is('parent_id', null)
|
|
179
|
+
|
|
180
|
+
const tasks = data ?? []
|
|
181
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
182
|
+
return {
|
|
183
|
+
total: tasks.length,
|
|
184
|
+
todo: tasks.filter((t: any) => t.status === 'todo').length,
|
|
185
|
+
inProgress: tasks.filter((t: any) => t.status === 'in_progress').length,
|
|
186
|
+
done: tasks.filter((t: any) => t.status === 'done').length,
|
|
187
|
+
overdue: tasks.filter((t: any) => t.due_date && t.due_date < today && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
188
|
+
dueToday: tasks.filter((t: any) => t.due_date === today && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
189
|
+
highPriority: tasks.filter((t: any) => (t.priority === 'high' || t.priority === 'urgent') && t.status !== 'done' && t.status !== 'cancelled').length,
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Task, TaskLabel, CreateTaskInput, UpdateTaskInput,
|
|
3
|
+
TaskQuery, TasksSummary, TaskStatus,
|
|
4
|
+
} from '../types'
|
|
5
|
+
|
|
6
|
+
export interface TasksDataProvider {
|
|
7
|
+
getTasks(query: TaskQuery): Promise<Task[]>
|
|
8
|
+
getTaskById(id: string): Promise<Task | null>
|
|
9
|
+
getSubtasks(parentId: string): Promise<Task[]>
|
|
10
|
+
createTask(input: CreateTaskInput): Promise<Task>
|
|
11
|
+
updateTask(id: string, input: UpdateTaskInput): Promise<Task>
|
|
12
|
+
deleteTask(id: string): Promise<void>
|
|
13
|
+
reorderTask(id: string, newPosition: number, status: TaskStatus): Promise<void>
|
|
14
|
+
getLabels(): Promise<TaskLabel[]>
|
|
15
|
+
getSummary(): Promise<TasksSummary>
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
+
import { TasksContextProvider, type ResolvedTasksConfig, type TasksPluginLabels } from './TasksContext'
|
|
4
|
+
import type { TasksDataProvider } from './data/types'
|
|
5
|
+
import type { TaskPriority } from './types'
|
|
6
|
+
import { createMockTasksProvider } from './data/mock'
|
|
7
|
+
import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
|
|
8
|
+
import { createSupabaseTasksProvider } from './data/supabase'
|
|
9
|
+
import { createTasksStore } from './store'
|
|
10
|
+
import { tasksLocales } from './locales'
|
|
11
|
+
import { tasksRegistries } from './registries'
|
|
12
|
+
import { TasksGeneralSettings } from './components/TasksGeneralSettings'
|
|
13
|
+
import { TasksTopbarButton } from './components/TasksTopbarButton'
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Public types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface TasksPluginOptions {
|
|
20
|
+
/** Label overrides */
|
|
21
|
+
labels?: Partial<TasksPluginLabels>
|
|
22
|
+
/** Plugin scope */
|
|
23
|
+
scope?: PluginScope
|
|
24
|
+
/** Vertical ID */
|
|
25
|
+
verticalId?: VerticalId
|
|
26
|
+
/** Data provider override (defaults to safe auto-selection) */
|
|
27
|
+
dataProvider?: TasksDataProvider
|
|
28
|
+
/** Default priority for new tasks (default: 'medium') */
|
|
29
|
+
defaultPriority?: TaskPriority
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Defaults
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const DEFAULT_LABELS: TasksPluginLabels = {
|
|
37
|
+
drawerTitle: 'Tasks',
|
|
38
|
+
settingsTitle: 'Tasks',
|
|
39
|
+
quickAddPlaceholder: 'Add a task...',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Config resolver
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function resolveConfig(options?: TasksPluginOptions): ResolvedTasksConfig {
|
|
47
|
+
return {
|
|
48
|
+
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Migration SQL
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const MIGRATION_001 = `-- Tasks Plugin: Base Tables
|
|
57
|
+
CREATE TABLE IF NOT EXISTS public.tsk_labels (
|
|
58
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
59
|
+
tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
|
|
60
|
+
name text NOT NULL,
|
|
61
|
+
color text NOT NULL DEFAULT '#6366f1',
|
|
62
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
63
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
64
|
+
);
|
|
65
|
+
ALTER TABLE public.tsk_labels ENABLE ROW LEVEL SECURITY;
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_labels_tenant ON public.tsk_labels(tenant_id);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS public.tsk_tasks (
|
|
69
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
70
|
+
tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
|
|
71
|
+
title text NOT NULL,
|
|
72
|
+
description text,
|
|
73
|
+
status text NOT NULL DEFAULT 'todo' CHECK (status IN ('todo','in_progress','done','cancelled')),
|
|
74
|
+
priority text NOT NULL DEFAULT 'medium' CHECK (priority IN ('low','medium','high','urgent')),
|
|
75
|
+
due_date date,
|
|
76
|
+
assigned_to_id uuid,
|
|
77
|
+
assigned_to_name text,
|
|
78
|
+
parent_id uuid REFERENCES public.tsk_tasks(id) ON DELETE CASCADE,
|
|
79
|
+
labels jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
80
|
+
position integer NOT NULL DEFAULT 0,
|
|
81
|
+
created_by_id uuid,
|
|
82
|
+
created_by_name text,
|
|
83
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
84
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
85
|
+
);
|
|
86
|
+
ALTER TABLE public.tsk_tasks ENABLE ROW LEVEL SECURITY;
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_tasks_tenant ON public.tsk_tasks(tenant_id);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_tasks_status ON public.tsk_tasks(tenant_id, status);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_tasks_parent ON public.tsk_tasks(parent_id);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_tasks_assigned ON public.tsk_tasks(assigned_to_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_tsk_tasks_due ON public.tsk_tasks(tenant_id, due_date) WHERE due_date IS NOT NULL;
|
|
92
|
+
|
|
93
|
+
-- RLS policies for tsk_tasks
|
|
94
|
+
CREATE POLICY tsk_tasks_select ON public.tsk_tasks FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
95
|
+
CREATE POLICY tsk_tasks_insert ON public.tsk_tasks FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
96
|
+
CREATE POLICY tsk_tasks_update ON public.tsk_tasks FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
97
|
+
CREATE POLICY tsk_tasks_delete ON public.tsk_tasks FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
98
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.tsk_tasks TO authenticated;
|
|
99
|
+
|
|
100
|
+
-- RLS policies for tsk_labels
|
|
101
|
+
CREATE POLICY tsk_labels_select ON public.tsk_labels FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
102
|
+
CREATE POLICY tsk_labels_insert ON public.tsk_labels FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
103
|
+
CREATE POLICY tsk_labels_update ON public.tsk_labels FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
104
|
+
CREATE POLICY tsk_labels_delete ON public.tsk_labels FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
105
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.tsk_labels TO authenticated;`
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Factory
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export function createTasksPlugin(options?: TasksPluginOptions): PluginManifest {
|
|
112
|
+
const config = resolveConfig(options)
|
|
113
|
+
// Register locales globally so the plugin's translations resolve even when the
|
|
114
|
+
// host shell does not mount @fayz-ai/core's I18nProvider (incremental de-bridge).
|
|
115
|
+
registerTranslations(tasksLocales)
|
|
116
|
+
const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
117
|
+
() => createSupabaseTasksProvider(),
|
|
118
|
+
() => createMockTasksProvider(),
|
|
119
|
+
)
|
|
120
|
+
const store = createTasksStore(provider)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: 'tasks',
|
|
124
|
+
name: config.labels.drawerTitle,
|
|
125
|
+
icon: 'CheckSquare',
|
|
126
|
+
version: '1.0.0',
|
|
127
|
+
scope: options?.scope ?? 'universal',
|
|
128
|
+
verticalId: options?.verticalId,
|
|
129
|
+
defaultEnabled: true,
|
|
130
|
+
dependencies: [],
|
|
131
|
+
|
|
132
|
+
navigation: [],
|
|
133
|
+
|
|
134
|
+
routes: [],
|
|
135
|
+
|
|
136
|
+
widgets: [
|
|
137
|
+
{
|
|
138
|
+
id: 'tasks-topbar-button',
|
|
139
|
+
zone: 'shell.topbar.end',
|
|
140
|
+
component: TasksTopbarButton as unknown as React.ComponentType<unknown>,
|
|
141
|
+
order: 10,
|
|
142
|
+
props: {
|
|
143
|
+
tasksConfig: config,
|
|
144
|
+
tasksProvider: provider,
|
|
145
|
+
tasksStore: store,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
aiTools: [
|
|
151
|
+
{
|
|
152
|
+
id: 'tasks.summary',
|
|
153
|
+
name: 'getTasksSummary',
|
|
154
|
+
description: 'Returns a summary of pending tasks: total, overdue, due today, by status and priority.',
|
|
155
|
+
icon: 'CheckSquare',
|
|
156
|
+
mode: 'read' as const,
|
|
157
|
+
category: 'Tasks',
|
|
158
|
+
parameters: {
|
|
159
|
+
type: 'object' as const,
|
|
160
|
+
properties: {
|
|
161
|
+
status: {
|
|
162
|
+
type: 'string' as const,
|
|
163
|
+
description: 'Filter by task status',
|
|
164
|
+
enum: ['todo', 'in_progress', 'done', 'cancelled', 'all'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
suggestions: [
|
|
169
|
+
{ label: 'What tasks are overdue?' },
|
|
170
|
+
{ label: 'How many tasks are pending?' },
|
|
171
|
+
{ label: "What's due today?" },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
|
|
176
|
+
registries: tasksRegistries,
|
|
177
|
+
|
|
178
|
+
settings: [
|
|
179
|
+
{
|
|
180
|
+
id: 'tasks',
|
|
181
|
+
label: config.labels.settingsTitle,
|
|
182
|
+
icon: 'CheckSquare',
|
|
183
|
+
component: (() => {
|
|
184
|
+
const Tab: React.FC = () =>
|
|
185
|
+
React.createElement(TasksContextProvider, { config, provider, store },
|
|
186
|
+
React.createElement(TasksGeneralSettings),
|
|
187
|
+
)
|
|
188
|
+
Tab.displayName = 'TasksSettingsTab'
|
|
189
|
+
return Tab as unknown as React.ComponentType<unknown>
|
|
190
|
+
})(),
|
|
191
|
+
order: 14,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
|
|
195
|
+
migrations: [
|
|
196
|
+
{
|
|
197
|
+
id: 'tasks-001-base-tables',
|
|
198
|
+
version: '1.0.0',
|
|
199
|
+
sql: MIGRATION_001,
|
|
200
|
+
description: 'Create tsk_tasks and tsk_labels tables',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
|
|
204
|
+
locales: tasksLocales,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Re-exports
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export type { TasksDataProvider } from './data/types'
|
|
213
|
+
export type { ResolvedTasksConfig, TasksPluginLabels } from './TasksContext'
|
|
214
|
+
export type {
|
|
215
|
+
Task, TaskLabel, TaskStatus, TaskPriority, TasksSummary,
|
|
216
|
+
CreateTaskInput, UpdateTaskInput, TaskQuery,
|
|
217
|
+
} from './types'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Current tenant id holder. The app/runtime sets this (runtime DI), decoupling
|
|
2
|
+
// the plugin from any specific org store — previously this read saas-core's
|
|
3
|
+
// organization.store. Defaults to undefined, in which case Supabase RLS scopes
|
|
4
|
+
// queries by the authenticated session.
|
|
5
|
+
let currentTenantId: string | undefined
|
|
6
|
+
|
|
7
|
+
export function setTasksTenantId(id: string | undefined): void {
|
|
8
|
+
currentTenantId = id
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTasksTenantId(): string | undefined {
|
|
12
|
+
return currentTenantId
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const en: Record<string, string> = {
|
|
2
|
+
'tasks.title': 'Tasks',
|
|
3
|
+
'tasks.drawer.title': 'Tasks',
|
|
4
|
+
'tasks.drawer.subtitle': 'Manage your team tasks',
|
|
5
|
+
'tasks.quickAdd.placeholder': 'Add a task...',
|
|
6
|
+
'tasks.filter.all': 'All',
|
|
7
|
+
'tasks.filter.todo': 'To Do',
|
|
8
|
+
'tasks.filter.inProgress': 'In Progress',
|
|
9
|
+
'tasks.filter.done': 'Done',
|
|
10
|
+
'tasks.status.todo': 'To Do',
|
|
11
|
+
'tasks.status.in_progress': 'In Progress',
|
|
12
|
+
'tasks.status.done': 'Done',
|
|
13
|
+
'tasks.status.cancelled': 'Cancelled',
|
|
14
|
+
'tasks.priority.low': 'Low',
|
|
15
|
+
'tasks.priority.medium': 'Medium',
|
|
16
|
+
'tasks.priority.high': 'High',
|
|
17
|
+
'tasks.priority.urgent': 'Urgent',
|
|
18
|
+
'tasks.detail.description': 'Description',
|
|
19
|
+
'tasks.detail.descriptionPlaceholder': 'Add a description...',
|
|
20
|
+
'tasks.detail.status': 'Status',
|
|
21
|
+
'tasks.detail.priority': 'Priority',
|
|
22
|
+
'tasks.detail.dueDate': 'Due Date',
|
|
23
|
+
'tasks.detail.assignee': 'Assigned To',
|
|
24
|
+
'tasks.detail.labels': 'Labels',
|
|
25
|
+
'tasks.detail.subtasks': 'Subtasks',
|
|
26
|
+
'tasks.detail.delete': 'Delete Task',
|
|
27
|
+
'tasks.detail.deleteConfirm': 'Are you sure you want to delete this task?',
|
|
28
|
+
'tasks.detail.created': 'Created',
|
|
29
|
+
'tasks.detail.updated': 'Updated',
|
|
30
|
+
'tasks.detail.addSubtask': 'Add subtask...',
|
|
31
|
+
'tasks.detail.back': 'Back to list',
|
|
32
|
+
'tasks.summary.overdue': 'overdue',
|
|
33
|
+
'tasks.summary.dueToday': 'due today',
|
|
34
|
+
'tasks.empty': 'No tasks yet',
|
|
35
|
+
'tasks.empty.description': 'Add your first task using the input above',
|
|
36
|
+
'tasks.settings.title': 'Tasks',
|
|
37
|
+
'tasks.settings.general': 'General',
|
|
38
|
+
'tasks.settings.defaultPriority': 'Default Priority',
|
|
39
|
+
'tasks.settings.autoAssign': 'Auto-assign tasks to creator',
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const ptBR: Record<string, string> = {
|
|
2
|
+
'tasks.title': 'Tarefas',
|
|
3
|
+
'tasks.drawer.title': 'Tarefas',
|
|
4
|
+
'tasks.drawer.subtitle': 'Gerencie as tarefas da equipe',
|
|
5
|
+
'tasks.quickAdd.placeholder': 'Adicionar tarefa...',
|
|
6
|
+
'tasks.filter.all': 'Todas',
|
|
7
|
+
'tasks.filter.todo': 'A Fazer',
|
|
8
|
+
'tasks.filter.inProgress': 'Em Andamento',
|
|
9
|
+
'tasks.filter.done': 'Concluídas',
|
|
10
|
+
'tasks.status.todo': 'A Fazer',
|
|
11
|
+
'tasks.status.in_progress': 'Em Andamento',
|
|
12
|
+
'tasks.status.done': 'Concluída',
|
|
13
|
+
'tasks.status.cancelled': 'Cancelada',
|
|
14
|
+
'tasks.priority.low': 'Baixa',
|
|
15
|
+
'tasks.priority.medium': 'Média',
|
|
16
|
+
'tasks.priority.high': 'Alta',
|
|
17
|
+
'tasks.priority.urgent': 'Urgente',
|
|
18
|
+
'tasks.detail.description': 'Descrição',
|
|
19
|
+
'tasks.detail.descriptionPlaceholder': 'Adicionar descrição...',
|
|
20
|
+
'tasks.detail.status': 'Status',
|
|
21
|
+
'tasks.detail.priority': 'Prioridade',
|
|
22
|
+
'tasks.detail.dueDate': 'Data de Vencimento',
|
|
23
|
+
'tasks.detail.assignee': 'Responsável',
|
|
24
|
+
'tasks.detail.labels': 'Etiquetas',
|
|
25
|
+
'tasks.detail.subtasks': 'Subtarefas',
|
|
26
|
+
'tasks.detail.delete': 'Excluir Tarefa',
|
|
27
|
+
'tasks.detail.deleteConfirm': 'Tem certeza que deseja excluir esta tarefa?',
|
|
28
|
+
'tasks.detail.created': 'Criada em',
|
|
29
|
+
'tasks.detail.updated': 'Atualizada em',
|
|
30
|
+
'tasks.detail.addSubtask': 'Adicionar subtarefa...',
|
|
31
|
+
'tasks.detail.back': 'Voltar à lista',
|
|
32
|
+
'tasks.summary.overdue': 'atrasadas',
|
|
33
|
+
'tasks.summary.dueToday': 'vencem hoje',
|
|
34
|
+
'tasks.empty': 'Nenhuma tarefa ainda',
|
|
35
|
+
'tasks.empty.description': 'Adicione sua primeira tarefa usando o campo acima',
|
|
36
|
+
'tasks.settings.title': 'Tarefas',
|
|
37
|
+
'tasks.settings.general': 'Geral',
|
|
38
|
+
'tasks.settings.defaultPriority': 'Prioridade Padrão',
|
|
39
|
+
'tasks.settings.autoAssign': 'Atribuir tarefas automaticamente ao criador',
|
|
40
|
+
}
|