realtimex-crm 0.13.8 → 0.14.0
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/assets/DealList-BrMu9tSP.js +59 -0
- package/dist/assets/DealList-BrMu9tSP.js.map +1 -0
- package/dist/assets/index-5AmasdLr.css +1 -0
- package/dist/assets/index-Ce7mf_H5.js +166 -0
- package/dist/assets/{index-BrW7DPxi.js.map → index-Ce7mf_H5.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/stats.html +1 -1
- package/package.json +1 -1
- package/src/components/atomic-crm/deals/DealCreate.tsx +5 -1
- package/src/components/atomic-crm/deals/DealShow.tsx +5 -1
- package/src/components/atomic-crm/integrations/WebhooksTab.tsx +10 -0
- package/src/components/atomic-crm/layout/Header.tsx +6 -0
- package/src/components/atomic-crm/notes/NoteCreate.tsx +3 -2
- package/src/components/atomic-crm/notes/NotesIterator.tsx +1 -1
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +4 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/taskActivity.ts +62 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/taskNotes.ts +29 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/tasks.ts +33 -8
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +4 -0
- package/src/components/atomic-crm/providers/supabase/dataProvider.ts +37 -0
- package/src/components/atomic-crm/root/CRM.tsx +12 -1
- package/src/components/atomic-crm/root/ConfigurationContext.tsx +10 -0
- package/src/components/atomic-crm/root/defaultConfiguration.ts +15 -0
- package/src/components/atomic-crm/tasks/MyTasksInput.tsx +30 -0
- package/src/components/atomic-crm/tasks/Task.tsx +20 -9
- package/src/components/atomic-crm/tasks/TaskActivityTimeline.tsx +91 -0
- package/src/components/atomic-crm/tasks/TaskAside.tsx +122 -0
- package/src/components/atomic-crm/tasks/TaskCreate.tsx +112 -0
- package/src/components/atomic-crm/tasks/TaskEdit.tsx +20 -1
- package/src/components/atomic-crm/tasks/TaskList.tsx +52 -0
- package/src/components/atomic-crm/tasks/TaskListTable.tsx +60 -0
- package/src/components/atomic-crm/tasks/TaskPriorityBadge.tsx +20 -0
- package/src/components/atomic-crm/tasks/TaskShow.tsx +71 -0
- package/src/components/atomic-crm/tasks/TaskStatusBadge.tsx +21 -0
- package/src/components/atomic-crm/tasks/index.ts +9 -0
- package/src/components/atomic-crm/types.ts +50 -0
- package/src/components/ui/visually-hidden.tsx +10 -0
- package/supabase/migrations/20251225120000_enhance_tasks_schema.sql +111 -0
- package/supabase/migrations/20251225120001_enhance_tasks_logic.sql +109 -0
- package/supabase/migrations/20251225120002_enhance_tasks_webhooks.sql +72 -0
- package/supabase/migrations/20251225150000_add_taskNotes_attachments.sql +6 -0
- package/dist/assets/DealList-CyjZCmZS.js +0 -59
- package/dist/assets/DealList-CyjZCmZS.js.map +0 -1
- package/dist/assets/index-BrW7DPxi.js +0 -166
- package/dist/assets/index-u4GyWWrL.css +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
-- 1.1 Task Notes Table
|
|
2
|
+
CREATE TABLE IF NOT EXISTS public."taskNotes" (
|
|
3
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
4
|
+
task_id bigint NOT NULL REFERENCES public.tasks(id) ON DELETE CASCADE,
|
|
5
|
+
text text NOT NULL,
|
|
6
|
+
date timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
|
7
|
+
sales_id bigint NOT NULL REFERENCES public.sales(id),
|
|
8
|
+
status text DEFAULT 'cold'::text,
|
|
9
|
+
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
|
|
10
|
+
updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_taskNotes_task_id ON public."taskNotes"(task_id);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_taskNotes_sales_id ON public."taskNotes"(sales_id);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_taskNotes_date ON public."taskNotes"(date DESC);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_taskNotes_task_id_date ON public."taskNotes"(task_id, date DESC);
|
|
17
|
+
|
|
18
|
+
ALTER TABLE public."taskNotes" ENABLE ROW LEVEL SECURITY;
|
|
19
|
+
|
|
20
|
+
CREATE POLICY "Enable read access for all authenticated users" ON public."taskNotes"
|
|
21
|
+
FOR SELECT USING (auth.role() = 'authenticated');
|
|
22
|
+
|
|
23
|
+
CREATE POLICY "Enable insert for authenticated users" ON public."taskNotes"
|
|
24
|
+
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
|
25
|
+
|
|
26
|
+
CREATE POLICY "Enable update for authenticated users" ON public."taskNotes"
|
|
27
|
+
FOR UPDATE USING (auth.role() = 'authenticated');
|
|
28
|
+
|
|
29
|
+
CREATE POLICY "Enable delete for authenticated users" ON public."taskNotes"
|
|
30
|
+
FOR DELETE USING (auth.role() = 'authenticated');
|
|
31
|
+
|
|
32
|
+
-- Ensure update_updated_at_column function exists
|
|
33
|
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
34
|
+
RETURNS TRIGGER AS $$
|
|
35
|
+
BEGIN
|
|
36
|
+
NEW.updated_at = timezone('utc'::text, now());
|
|
37
|
+
RETURN NEW;
|
|
38
|
+
END;
|
|
39
|
+
$$ language 'plpgsql';
|
|
40
|
+
|
|
41
|
+
CREATE TRIGGER update_taskNotes_updated_at
|
|
42
|
+
BEFORE UPDATE ON public."taskNotes"
|
|
43
|
+
FOR EACH ROW
|
|
44
|
+
EXECUTE FUNCTION update_updated_at_column();
|
|
45
|
+
|
|
46
|
+
-- 1.2 Enhanced Tasks Table
|
|
47
|
+
ALTER TABLE public.tasks
|
|
48
|
+
ADD COLUMN IF NOT EXISTS priority text DEFAULT 'medium',
|
|
49
|
+
ADD COLUMN IF NOT EXISTS assigned_to bigint REFERENCES public.sales(id),
|
|
50
|
+
ADD COLUMN IF NOT EXISTS status text DEFAULT 'todo',
|
|
51
|
+
ADD COLUMN IF NOT EXISTS created_at timestamp with time zone DEFAULT timezone('utc'::text, now()),
|
|
52
|
+
ADD COLUMN IF NOT EXISTS updated_at timestamp with time zone DEFAULT timezone('utc'::text, now());
|
|
53
|
+
|
|
54
|
+
COMMENT ON COLUMN public.tasks.priority IS 'Task priority: low, medium, high, urgent';
|
|
55
|
+
COMMENT ON COLUMN public.tasks.assigned_to IS 'Sales person assigned to task (nullable for unassigned tasks)';
|
|
56
|
+
COMMENT ON COLUMN public.tasks.status IS 'Task status: todo, in_progress, blocked, done, cancelled';
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON public.tasks(assigned_to);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON public.tasks(status);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON public.tasks(priority);
|
|
61
|
+
|
|
62
|
+
CREATE TRIGGER update_tasks_updated_at
|
|
63
|
+
BEFORE UPDATE ON public.tasks
|
|
64
|
+
FOR EACH ROW
|
|
65
|
+
EXECUTE FUNCTION update_updated_at_column();
|
|
66
|
+
|
|
67
|
+
-- Update existing tasks
|
|
68
|
+
UPDATE public.tasks
|
|
69
|
+
SET status = CASE
|
|
70
|
+
WHEN done_date IS NOT NULL THEN 'done'
|
|
71
|
+
ELSE 'todo'
|
|
72
|
+
END
|
|
73
|
+
WHERE status IS NULL OR status = 'todo';
|
|
74
|
+
|
|
75
|
+
UPDATE public.tasks
|
|
76
|
+
SET assigned_to = sales_id
|
|
77
|
+
WHERE assigned_to IS NULL AND sales_id IS NOT NULL;
|
|
78
|
+
|
|
79
|
+
-- 1.4 Task Activity Log
|
|
80
|
+
CREATE TABLE IF NOT EXISTS public.task_activity (
|
|
81
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
82
|
+
task_id bigint NOT NULL REFERENCES public.tasks(id) ON DELETE CASCADE,
|
|
83
|
+
sales_id bigint NOT NULL REFERENCES public.sales(id),
|
|
84
|
+
action text NOT NULL,
|
|
85
|
+
field_name text,
|
|
86
|
+
old_value text,
|
|
87
|
+
new_value text,
|
|
88
|
+
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_task_id ON public.task_activity(task_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_sales_id ON public.task_activity(sales_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_created_at ON public.task_activity(created_at DESC);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_task_created ON public.task_activity(task_id, created_at DESC);
|
|
95
|
+
|
|
96
|
+
COMMENT ON COLUMN public.task_activity.action IS 'Action performed: created, updated, assigned, completed, deleted, duplicated, archived';
|
|
97
|
+
|
|
98
|
+
ALTER TABLE public.task_activity ENABLE ROW LEVEL SECURITY;
|
|
99
|
+
|
|
100
|
+
CREATE POLICY "Enable read access for all authenticated users" ON public.task_activity
|
|
101
|
+
FOR SELECT USING (auth.role() = 'authenticated');
|
|
102
|
+
|
|
103
|
+
CREATE POLICY "Enable insert for authenticated users" ON public.task_activity
|
|
104
|
+
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
|
105
|
+
|
|
106
|
+
-- 1.5 Task Archiving Columns
|
|
107
|
+
ALTER TABLE public.tasks
|
|
108
|
+
ADD COLUMN IF NOT EXISTS archived boolean DEFAULT false,
|
|
109
|
+
ADD COLUMN IF NOT EXISTS archived_at timestamp with time zone;
|
|
110
|
+
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_archived ON public.tasks(archived, archived_at);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- 1.3 Tasks Summary View
|
|
2
|
+
CREATE OR REPLACE VIEW public.tasks_summary AS
|
|
3
|
+
SELECT
|
|
4
|
+
t.id,
|
|
5
|
+
t.contact_id,
|
|
6
|
+
t.type,
|
|
7
|
+
t.text,
|
|
8
|
+
t.due_date,
|
|
9
|
+
t.done_date,
|
|
10
|
+
t.sales_id,
|
|
11
|
+
t.priority,
|
|
12
|
+
t.assigned_to,
|
|
13
|
+
t.status,
|
|
14
|
+
t.created_at,
|
|
15
|
+
t.updated_at,
|
|
16
|
+
t.archived,
|
|
17
|
+
t.archived_at,
|
|
18
|
+
-- Contact information
|
|
19
|
+
c.first_name as contact_first_name,
|
|
20
|
+
c.last_name as contact_last_name,
|
|
21
|
+
c.email_jsonb->0->>'email' as contact_email,
|
|
22
|
+
c.company_id,
|
|
23
|
+
comp.name as company_name,
|
|
24
|
+
-- Assigned sales person
|
|
25
|
+
s_assigned.first_name as assigned_first_name,
|
|
26
|
+
s_assigned.last_name as assigned_last_name,
|
|
27
|
+
-- Creator sales person
|
|
28
|
+
s_creator.first_name as creator_first_name,
|
|
29
|
+
s_creator.last_name as creator_last_name,
|
|
30
|
+
-- Task note count
|
|
31
|
+
count(DISTINCT tn.id) as nb_notes,
|
|
32
|
+
-- Most recent note
|
|
33
|
+
max(tn.date) as last_note_date
|
|
34
|
+
FROM public.tasks t
|
|
35
|
+
LEFT JOIN public.contacts c ON t.contact_id = c.id
|
|
36
|
+
LEFT JOIN public.companies comp ON c.company_id = comp.id
|
|
37
|
+
LEFT JOIN public.sales s_assigned ON t.assigned_to = s_assigned.id
|
|
38
|
+
LEFT JOIN public.sales s_creator ON t.sales_id = s_creator.id
|
|
39
|
+
LEFT JOIN public."taskNotes" tn ON t.id = tn.task_id
|
|
40
|
+
GROUP BY
|
|
41
|
+
t.id,
|
|
42
|
+
c.first_name, c.last_name, c.email_jsonb, c.company_id,
|
|
43
|
+
comp.name,
|
|
44
|
+
s_assigned.first_name, s_assigned.last_name,
|
|
45
|
+
s_creator.first_name, s_creator.last_name;
|
|
46
|
+
|
|
47
|
+
GRANT SELECT ON public.tasks_summary TO authenticated;
|
|
48
|
+
|
|
49
|
+
-- 1.5 Archiving Function
|
|
50
|
+
CREATE OR REPLACE FUNCTION archive_completed_tasks(days_threshold integer DEFAULT 30)
|
|
51
|
+
RETURNS integer AS $$
|
|
52
|
+
DECLARE
|
|
53
|
+
archived_count integer;
|
|
54
|
+
BEGIN
|
|
55
|
+
WITH archived AS (
|
|
56
|
+
UPDATE public.tasks
|
|
57
|
+
SET archived = true,
|
|
58
|
+
archived_at = NOW()
|
|
59
|
+
WHERE status = 'done'
|
|
60
|
+
AND done_date IS NOT NULL
|
|
61
|
+
AND done_date < NOW() - (days_threshold || ' days')::interval
|
|
62
|
+
AND archived = false
|
|
63
|
+
RETURNING id
|
|
64
|
+
)
|
|
65
|
+
SELECT COUNT(*) INTO archived_count FROM archived;
|
|
66
|
+
|
|
67
|
+
RETURN archived_count;
|
|
68
|
+
END;
|
|
69
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
70
|
+
|
|
71
|
+
GRANT EXECUTE ON FUNCTION archive_completed_tasks TO authenticated;
|
|
72
|
+
|
|
73
|
+
-- 1.6 Real-Time
|
|
74
|
+
-- tasks table is already enabled for realtime
|
|
75
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public."taskNotes";
|
|
76
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.task_activity;
|
|
77
|
+
|
|
78
|
+
-- Sync Logic for Status and Done Date (Section 7)
|
|
79
|
+
CREATE OR REPLACE FUNCTION sync_task_status_and_done_date()
|
|
80
|
+
RETURNS TRIGGER AS $$
|
|
81
|
+
BEGIN
|
|
82
|
+
-- If status changed to 'done', set done_date
|
|
83
|
+
IF NEW.status = 'done' AND (OLD.status IS DISTINCT FROM 'done') THEN
|
|
84
|
+
NEW.done_date = NOW();
|
|
85
|
+
END IF;
|
|
86
|
+
|
|
87
|
+
-- If status changed from 'done', clear done_date
|
|
88
|
+
IF NEW.status != 'done' AND OLD.status = 'done' THEN
|
|
89
|
+
NEW.done_date = NULL;
|
|
90
|
+
END IF;
|
|
91
|
+
|
|
92
|
+
-- If done_date set, update status to 'done'
|
|
93
|
+
IF NEW.done_date IS NOT NULL AND OLD.done_date IS NULL THEN
|
|
94
|
+
NEW.status = 'done';
|
|
95
|
+
END IF;
|
|
96
|
+
|
|
97
|
+
-- If done_date cleared, update status away from 'done'
|
|
98
|
+
IF NEW.done_date IS NULL AND OLD.done_date IS NOT NULL THEN
|
|
99
|
+
NEW.status = 'todo';
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
RETURN NEW;
|
|
103
|
+
END;
|
|
104
|
+
$$ LANGUAGE plpgsql;
|
|
105
|
+
|
|
106
|
+
CREATE TRIGGER sync_task_status_trigger
|
|
107
|
+
BEFORE UPDATE ON public.tasks
|
|
108
|
+
FOR EACH ROW
|
|
109
|
+
EXECUTE FUNCTION sync_task_status_and_done_date();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- 1.7 Extend Task Webhooks
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION trigger_task_webhooks()
|
|
4
|
+
RETURNS TRIGGER AS $$
|
|
5
|
+
DECLARE
|
|
6
|
+
specific_event_fired boolean := false;
|
|
7
|
+
BEGIN
|
|
8
|
+
-- Task created
|
|
9
|
+
IF TG_OP = 'INSERT' THEN
|
|
10
|
+
PERFORM enqueue_webhook_event('task.created', to_jsonb(NEW));
|
|
11
|
+
RETURN NEW;
|
|
12
|
+
END IF;
|
|
13
|
+
|
|
14
|
+
-- Task updated
|
|
15
|
+
IF TG_OP = 'UPDATE' THEN
|
|
16
|
+
-- Task completed (existing logic - keep for backward compatibility)
|
|
17
|
+
IF OLD.done_date IS NULL AND NEW.done_date IS NOT NULL THEN
|
|
18
|
+
PERFORM enqueue_webhook_event('task.completed', to_jsonb(NEW));
|
|
19
|
+
specific_event_fired := true;
|
|
20
|
+
END IF;
|
|
21
|
+
|
|
22
|
+
-- Task assigned/reassigned
|
|
23
|
+
IF OLD.assigned_to IS DISTINCT FROM NEW.assigned_to THEN
|
|
24
|
+
PERFORM enqueue_webhook_event('task.assigned', jsonb_build_object(
|
|
25
|
+
'task', to_jsonb(NEW),
|
|
26
|
+
'old_assignee', OLD.assigned_to,
|
|
27
|
+
'new_assignee', NEW.assigned_to
|
|
28
|
+
));
|
|
29
|
+
specific_event_fired := true;
|
|
30
|
+
END IF;
|
|
31
|
+
|
|
32
|
+
-- Task archived
|
|
33
|
+
IF OLD.archived IS DISTINCT FROM NEW.archived AND NEW.archived = true THEN
|
|
34
|
+
PERFORM enqueue_webhook_event('task.archived', to_jsonb(NEW));
|
|
35
|
+
specific_event_fired := true;
|
|
36
|
+
END IF;
|
|
37
|
+
|
|
38
|
+
-- Task priority changed
|
|
39
|
+
IF OLD.priority IS DISTINCT FROM NEW.priority THEN
|
|
40
|
+
PERFORM enqueue_webhook_event('task.priority_changed', jsonb_build_object(
|
|
41
|
+
'task', to_jsonb(NEW),
|
|
42
|
+
'old_priority', OLD.priority,
|
|
43
|
+
'new_priority', NEW.priority
|
|
44
|
+
));
|
|
45
|
+
specific_event_fired := true;
|
|
46
|
+
END IF;
|
|
47
|
+
|
|
48
|
+
-- Generic task.updated (only fires if no specific event matched)
|
|
49
|
+
-- This reduces webhook noise while still catching other field changes
|
|
50
|
+
IF NOT specific_event_fired THEN
|
|
51
|
+
PERFORM enqueue_webhook_event('task.updated', jsonb_build_object(
|
|
52
|
+
'old', to_jsonb(OLD),
|
|
53
|
+
'new', to_jsonb(NEW)
|
|
54
|
+
));
|
|
55
|
+
END IF;
|
|
56
|
+
END IF;
|
|
57
|
+
|
|
58
|
+
-- Task deleted
|
|
59
|
+
IF TG_OP = 'DELETE' THEN
|
|
60
|
+
PERFORM enqueue_webhook_event('task.deleted', to_jsonb(OLD));
|
|
61
|
+
RETURN OLD;
|
|
62
|
+
END IF;
|
|
63
|
+
|
|
64
|
+
RETURN NEW;
|
|
65
|
+
END;
|
|
66
|
+
$$ LANGUAGE plpgsql;
|
|
67
|
+
|
|
68
|
+
-- Trigger already exists in previous migrations, but we can recreate it to be safe or ensure it covers INSERT/DELETE
|
|
69
|
+
DROP TRIGGER IF EXISTS tasks_webhook_trigger ON public.tasks;
|
|
70
|
+
CREATE TRIGGER tasks_webhook_trigger
|
|
71
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.tasks
|
|
72
|
+
FOR EACH ROW EXECUTE FUNCTION trigger_task_webhooks();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Add attachments column to taskNotes table to match contactNotes and dealNotes
|
|
2
|
+
|
|
3
|
+
ALTER TABLE public."taskNotes"
|
|
4
|
+
ADD COLUMN IF NOT EXISTS attachments jsonb[];
|
|
5
|
+
|
|
6
|
+
COMMENT ON COLUMN public."taskNotes".attachments IS 'Array of attachment metadata (file URLs, names, types)';
|