realtimex-crm 0.13.8 → 0.14.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/assets/DealList-CRBXwi1I.js +59 -0
- package/dist/assets/DealList-CRBXwi1I.js.map +1 -0
- package/dist/assets/index-5AmasdLr.css +1 -0
- package/dist/assets/index-D5uatqhL.js +166 -0
- package/dist/assets/{index-BrW7DPxi.js.map → index-D5uatqhL.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/20251225085142_enhance_companies.sql +3 -3
- package/supabase/migrations/{20251226120000_fix_ambiguous_column.sql → 20251225110000_fix_ambiguous_column.sql} +2 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "RealTimeX CRM - A full-featured CRM built with React, shadcn-admin-kit, and Supabase. Fork of Atomic CRM with RealTimeX App SDK integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
import { Create } from "@/components/admin/create";
|
|
11
11
|
import { SaveButton } from "@/components/admin/form";
|
|
12
12
|
import { FormToolbar } from "@/components/admin/simple-form";
|
|
13
|
-
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
13
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
14
|
+
import { VisuallyHidden } from "@/components/ui/visually-hidden";
|
|
14
15
|
|
|
15
16
|
import type { Deal } from "../types";
|
|
16
17
|
import { DealInputs } from "./DealInputs";
|
|
@@ -75,6 +76,9 @@ export const DealCreate = ({ open }: { open: boolean }) => {
|
|
|
75
76
|
return (
|
|
76
77
|
<Dialog open={open} onOpenChange={() => handleClose()}>
|
|
77
78
|
<DialogContent className="lg:max-w-4xl overflow-y-auto max-h-9/10 top-1/20 translate-y-0">
|
|
79
|
+
<VisuallyHidden>
|
|
80
|
+
<DialogTitle>Create Deal</DialogTitle>
|
|
81
|
+
</VisuallyHidden>
|
|
78
82
|
<Create resource="deals" mutationOptions={{ onSuccess }}>
|
|
79
83
|
<Form
|
|
80
84
|
defaultValues={{
|
|
@@ -17,8 +17,9 @@ import { ReferenceField } from "@/components/admin/reference-field";
|
|
|
17
17
|
import { ReferenceManyField } from "@/components/admin/reference-many-field";
|
|
18
18
|
import { Badge } from "@/components/ui/badge";
|
|
19
19
|
import { Button } from "@/components/ui/button";
|
|
20
|
-
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
20
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
21
21
|
import { Separator } from "@/components/ui/separator";
|
|
22
|
+
import { VisuallyHidden } from "@/components/ui/visually-hidden";
|
|
22
23
|
|
|
23
24
|
import { CompanyAvatar } from "../companies/CompanyAvatar";
|
|
24
25
|
import { NoteCreate } from "../notes/NoteCreate";
|
|
@@ -37,6 +38,9 @@ export const DealShow = ({ open, id }: { open: boolean; id?: string }) => {
|
|
|
37
38
|
return (
|
|
38
39
|
<Dialog open={open} onOpenChange={(open) => !open && handleClose()}>
|
|
39
40
|
<DialogContent className="lg:max-w-4xl p-4 overflow-y-auto max-h-9/10 top-1/20 translate-y-0">
|
|
41
|
+
<VisuallyHidden>
|
|
42
|
+
<DialogTitle>Deal Details</DialogTitle>
|
|
43
|
+
</VisuallyHidden>
|
|
40
44
|
{id ? (
|
|
41
45
|
<ShowBase id={id}>
|
|
42
46
|
<DealShowContent />
|
|
@@ -46,7 +46,17 @@ const AVAILABLE_EVENTS = [
|
|
|
46
46
|
},
|
|
47
47
|
{ value: "deal.won", label: "Deal Won", category: "Deals" },
|
|
48
48
|
{ value: "deal.lost", label: "Deal Lost", category: "Deals" },
|
|
49
|
+
{ value: "task.created", label: "Task Created", category: "Tasks" },
|
|
50
|
+
{ value: "task.updated", label: "Task Updated", category: "Tasks" },
|
|
51
|
+
{ value: "task.assigned", label: "Task Assigned", category: "Tasks" },
|
|
49
52
|
{ value: "task.completed", label: "Task Completed", category: "Tasks" },
|
|
53
|
+
{
|
|
54
|
+
value: "task.priority_changed",
|
|
55
|
+
label: "Task Priority Changed",
|
|
56
|
+
category: "Tasks",
|
|
57
|
+
},
|
|
58
|
+
{ value: "task.archived", label: "Task Archived", category: "Tasks" },
|
|
59
|
+
{ value: "task.deleted", label: "Task Deleted", category: "Tasks" },
|
|
50
60
|
];
|
|
51
61
|
|
|
52
62
|
export const WebhooksTab = () => {
|
|
@@ -23,6 +23,7 @@ const Header = () => {
|
|
|
23
23
|
{ path: "/contacts", pattern: "/contacts/*" },
|
|
24
24
|
{ path: "/companies", pattern: "/companies/*" },
|
|
25
25
|
{ path: "/deals", pattern: "/deals/*" },
|
|
26
|
+
{ path: "/tasks", pattern: "/tasks/*" },
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
const currentPath =
|
|
@@ -56,6 +57,11 @@ const Header = () => {
|
|
|
56
57
|
to="/deals"
|
|
57
58
|
isActive={currentPath === "/deals"}
|
|
58
59
|
/>
|
|
60
|
+
<NavigationTab
|
|
61
|
+
label="Tasks"
|
|
62
|
+
to="/tasks"
|
|
63
|
+
isActive={currentPath === "/tasks"}
|
|
64
|
+
/>
|
|
59
65
|
</nav>
|
|
60
66
|
|
|
61
67
|
<div className="flex items-center">
|
|
@@ -21,6 +21,7 @@ const foreignKeyMapping = {
|
|
|
21
21
|
contacts: "contact_id",
|
|
22
22
|
deals: "deal_id",
|
|
23
23
|
companies: "company_id",
|
|
24
|
+
tasks: "task_id",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const NoteCreate = ({
|
|
@@ -28,7 +29,7 @@ export const NoteCreate = ({
|
|
|
28
29
|
showStatus,
|
|
29
30
|
className,
|
|
30
31
|
}: {
|
|
31
|
-
reference: "contacts" | "deals" | "companies";
|
|
32
|
+
reference: "contacts" | "deals" | "companies" | "tasks";
|
|
32
33
|
showStatus?: boolean;
|
|
33
34
|
className?: string;
|
|
34
35
|
}) => {
|
|
@@ -54,7 +55,7 @@ const NoteCreateButton = ({
|
|
|
54
55
|
reference,
|
|
55
56
|
record,
|
|
56
57
|
}: {
|
|
57
|
-
reference: "contacts" | "deals" | "companies";
|
|
58
|
+
reference: "contacts" | "deals" | "companies" | "tasks";
|
|
58
59
|
record: RaRecord<Identifier>;
|
|
59
60
|
}) => {
|
|
60
61
|
const [update] = useUpdate();
|
|
@@ -9,7 +9,7 @@ export const NotesIterator = ({
|
|
|
9
9
|
reference,
|
|
10
10
|
showStatus,
|
|
11
11
|
}: {
|
|
12
|
-
reference: "contacts" | "deals" | "companies";
|
|
12
|
+
reference: "contacts" | "deals" | "companies" | "tasks";
|
|
13
13
|
showStatus?: boolean;
|
|
14
14
|
}) => {
|
|
15
15
|
const { data, error, isPending } = useListContext();
|
|
@@ -7,6 +7,8 @@ import { generateDeals } from "./deals";
|
|
|
7
7
|
import { finalize } from "./finalize";
|
|
8
8
|
import { generateSales } from "./sales";
|
|
9
9
|
import { generateTags } from "./tags";
|
|
10
|
+
import { generateTaskActivity } from "./taskActivity";
|
|
11
|
+
import { generateTaskNotes } from "./taskNotes";
|
|
10
12
|
import { generateTasks } from "./tasks";
|
|
11
13
|
import type { Db } from "./types";
|
|
12
14
|
|
|
@@ -21,6 +23,8 @@ export default (): Db => {
|
|
|
21
23
|
db.deals = generateDeals(db);
|
|
22
24
|
db.dealNotes = generateDealNotes(db);
|
|
23
25
|
db.tasks = generateTasks(db);
|
|
26
|
+
db.taskNotes = generateTaskNotes(db);
|
|
27
|
+
db.taskActivity = generateTaskActivity(db);
|
|
24
28
|
finalize(db);
|
|
25
29
|
|
|
26
30
|
return db;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { datatype, random } from "faker/locale/en_US";
|
|
2
|
+
import type { TaskActivity } from "../../../types";
|
|
3
|
+
import type { Db } from "./types";
|
|
4
|
+
import { randomDate } from "./utils";
|
|
5
|
+
|
|
6
|
+
export const generateTaskActivity = (db: Db): TaskActivity[] => {
|
|
7
|
+
const activities: TaskActivity[] = [];
|
|
8
|
+
const actionsPool = ['updated', 'assigned', 'completed', 'reopened'];
|
|
9
|
+
const fieldsPool = ['text', 'priority', 'status', 'due_date'];
|
|
10
|
+
let id = 0;
|
|
11
|
+
|
|
12
|
+
db.tasks.forEach((task) => {
|
|
13
|
+
// Always create "created" activity
|
|
14
|
+
const createdAt = task.created_at || new Date().toISOString();
|
|
15
|
+
|
|
16
|
+
activities.push({
|
|
17
|
+
id: id++,
|
|
18
|
+
task_id: task.id,
|
|
19
|
+
sales_id: task.sales_id || db.sales[0].id,
|
|
20
|
+
action: 'created',
|
|
21
|
+
field_name: undefined,
|
|
22
|
+
old_value: undefined,
|
|
23
|
+
new_value: undefined,
|
|
24
|
+
created_at: createdAt,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Generate 1-5 random activities per task
|
|
28
|
+
const activityCount = datatype.number({min: 1, max: 5});
|
|
29
|
+
for (let i = 0; i < activityCount; i++) {
|
|
30
|
+
const action = random.arrayElement(actionsPool);
|
|
31
|
+
const field = action === 'updated' ? random.arrayElement(fieldsPool) : undefined;
|
|
32
|
+
const date = randomDate(new Date(createdAt));
|
|
33
|
+
|
|
34
|
+
activities.push({
|
|
35
|
+
id: id++,
|
|
36
|
+
task_id: task.id,
|
|
37
|
+
sales_id: random.arrayElement(db.sales).id,
|
|
38
|
+
action,
|
|
39
|
+
field_name: field,
|
|
40
|
+
old_value: field ? 'medium' : undefined,
|
|
41
|
+
new_value: field ? 'high' : undefined,
|
|
42
|
+
created_at: date.toISOString(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add "completed" activity if task is done
|
|
47
|
+
if (task.done_date) {
|
|
48
|
+
activities.push({
|
|
49
|
+
id: id++,
|
|
50
|
+
task_id: task.id,
|
|
51
|
+
sales_id: task.sales_id || db.sales[0].id,
|
|
52
|
+
action: 'completed',
|
|
53
|
+
field_name: undefined,
|
|
54
|
+
old_value: undefined,
|
|
55
|
+
new_value: undefined,
|
|
56
|
+
created_at: task.done_date,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return activities;
|
|
62
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { lorem } from "faker/locale/en_US";
|
|
2
|
+
import type { TaskNote } from "../../../types";
|
|
3
|
+
import type { Db } from "./types";
|
|
4
|
+
import { randomDate } from "./utils";
|
|
5
|
+
|
|
6
|
+
export const generateTaskNotes = (db: Db): TaskNote[] => {
|
|
7
|
+
const taskNotes: TaskNote[] = [];
|
|
8
|
+
let id = 0;
|
|
9
|
+
|
|
10
|
+
db.tasks.forEach((task) => {
|
|
11
|
+
// Generate 0-3 notes per task
|
|
12
|
+
const noteCount = Math.floor(Math.random() * 4);
|
|
13
|
+
for (let i = 0; i < noteCount; i++) {
|
|
14
|
+
const date = randomDate(new Date(task.created_at || new Date()));
|
|
15
|
+
taskNotes.push({
|
|
16
|
+
id: id++,
|
|
17
|
+
task_id: task.id,
|
|
18
|
+
text: lorem.paragraph(),
|
|
19
|
+
date: date.toISOString(),
|
|
20
|
+
sales_id: task.sales_id || db.sales[0].id,
|
|
21
|
+
status: ["cold", "warm", "hot"][Math.floor(Math.random() * 3)],
|
|
22
|
+
created_at: date.toISOString(),
|
|
23
|
+
updated_at: date.toISOString(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return taskNotes;
|
|
29
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { datatype, lorem, random } from "faker/locale/en_US";
|
|
2
2
|
|
|
3
3
|
import { defaultTaskTypes } from "../../../root/defaultConfiguration";
|
|
4
|
-
import type { Task } from "../../../types";
|
|
5
4
|
import type { Db } from "./types";
|
|
6
5
|
import { randomDate } from "./utils";
|
|
7
6
|
|
|
@@ -36,20 +35,46 @@ export const type: TaskType[] = [
|
|
|
36
35
|
];
|
|
37
36
|
|
|
38
37
|
export const generateTasks = (db: Db) => {
|
|
39
|
-
return Array.from(Array(400).keys()).map<
|
|
38
|
+
return Array.from(Array(400).keys()).map<any>((id) => {
|
|
40
39
|
const contact = random.arrayElement(db.contacts);
|
|
40
|
+
const company = db.companies.find((c) => c.id === contact.company_id);
|
|
41
|
+
const creator = db.sales.find((s) => s.id === contact.sales_id);
|
|
41
42
|
contact.nb_tasks++;
|
|
43
|
+
|
|
44
|
+
const createdDate = randomDate(new Date(contact.first_seen)).toISOString();
|
|
45
|
+
const dueDate = randomDate(
|
|
46
|
+
datatype.boolean() ? new Date() : new Date(contact.first_seen),
|
|
47
|
+
new Date(Date.now() + 100 * 24 * 60 * 60 * 1000),
|
|
48
|
+
).toISOString();
|
|
49
|
+
|
|
50
|
+
const isDone = datatype.boolean();
|
|
51
|
+
const doneDate = isDone ? randomDate(new Date(createdDate)).toISOString() : undefined;
|
|
52
|
+
|
|
42
53
|
return {
|
|
43
54
|
id,
|
|
44
55
|
contact_id: contact.id,
|
|
45
56
|
type: random.arrayElement(defaultTaskTypes),
|
|
46
57
|
text: lorem.sentence(),
|
|
47
|
-
due_date:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
due_date: dueDate,
|
|
59
|
+
done_date: doneDate,
|
|
60
|
+
sales_id: contact.sales_id,
|
|
61
|
+
priority: random.arrayElement(["low", "medium", "high", "urgent"]),
|
|
62
|
+
assigned_to: contact.sales_id,
|
|
63
|
+
status: isDone ? "done" : "todo",
|
|
64
|
+
created_at: createdDate,
|
|
65
|
+
updated_at: createdDate,
|
|
66
|
+
archived: false,
|
|
67
|
+
|
|
68
|
+
// Denormalized fields for TaskSummary simulation
|
|
69
|
+
contact_first_name: contact.first_name,
|
|
70
|
+
contact_last_name: contact.last_name,
|
|
71
|
+
contact_email: contact.email_jsonb?.[0]?.email,
|
|
72
|
+
company_id: contact.company_id,
|
|
73
|
+
company_name: company?.name,
|
|
74
|
+
creator_first_name: creator?.first_name,
|
|
75
|
+
creator_last_name: creator?.last_name,
|
|
76
|
+
assigned_first_name: creator?.first_name,
|
|
77
|
+
assigned_last_name: creator?.last_name,
|
|
53
78
|
};
|
|
54
79
|
});
|
|
55
80
|
};
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
Sale,
|
|
9
9
|
Tag,
|
|
10
10
|
Task,
|
|
11
|
+
TaskNote,
|
|
12
|
+
TaskActivity,
|
|
11
13
|
} from "../../../types";
|
|
12
14
|
|
|
13
15
|
export interface Db {
|
|
@@ -20,4 +22,6 @@ export interface Db {
|
|
|
20
22
|
sales: Sale[];
|
|
21
23
|
tags: Tag[];
|
|
22
24
|
tasks: Task[];
|
|
25
|
+
taskNotes: TaskNote[];
|
|
26
|
+
taskActivity: TaskActivity[];
|
|
23
27
|
}
|
|
@@ -90,6 +90,9 @@ const dataProviderWithCustomMethods = {
|
|
|
90
90
|
if (resource === "contacts") {
|
|
91
91
|
return baseDataProvider.getList("contacts_summary", params);
|
|
92
92
|
}
|
|
93
|
+
if (resource === "tasks") {
|
|
94
|
+
return baseDataProvider.getList("tasks_summary", params);
|
|
95
|
+
}
|
|
93
96
|
|
|
94
97
|
return baseDataProvider.getList(resource, params);
|
|
95
98
|
},
|
|
@@ -100,10 +103,38 @@ const dataProviderWithCustomMethods = {
|
|
|
100
103
|
if (resource === "contacts") {
|
|
101
104
|
return baseDataProvider.getOne("contacts_summary", params);
|
|
102
105
|
}
|
|
106
|
+
if (resource === "tasks") {
|
|
107
|
+
return baseDataProvider.getOne("tasks_summary", params);
|
|
108
|
+
}
|
|
103
109
|
|
|
104
110
|
return baseDataProvider.getOne(resource, params);
|
|
105
111
|
},
|
|
106
112
|
|
|
113
|
+
async update(resource: string, params: any) {
|
|
114
|
+
if (resource === "tasks") {
|
|
115
|
+
const {
|
|
116
|
+
contact_first_name: _contact_first_name,
|
|
117
|
+
contact_last_name: _contact_last_name,
|
|
118
|
+
contact_email: _contact_email,
|
|
119
|
+
company_id: _company_id,
|
|
120
|
+
company_name: _company_name,
|
|
121
|
+
assigned_first_name: _assigned_first_name,
|
|
122
|
+
assigned_last_name: _assigned_last_name,
|
|
123
|
+
creator_first_name: _creator_first_name,
|
|
124
|
+
creator_last_name: _creator_last_name,
|
|
125
|
+
nb_notes: _nb_notes,
|
|
126
|
+
last_note_date: _last_note_date,
|
|
127
|
+
...data
|
|
128
|
+
} = params.data;
|
|
129
|
+
|
|
130
|
+
return baseDataProvider.update(resource, {
|
|
131
|
+
...params,
|
|
132
|
+
data,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return baseDataProvider.update(resource, params);
|
|
136
|
+
},
|
|
137
|
+
|
|
107
138
|
async signUp({ email, password, first_name, last_name }: SignUpData) {
|
|
108
139
|
// Use admin API via edge function to create first user
|
|
109
140
|
// This bypasses the signup restriction (enable_signup = false)
|
|
@@ -371,6 +402,12 @@ export const dataProvider = withLifecycleCallbacks(
|
|
|
371
402
|
return applyFullTextSearch(["first_name", "last_name"])(params);
|
|
372
403
|
},
|
|
373
404
|
},
|
|
405
|
+
{
|
|
406
|
+
resource: "tasks",
|
|
407
|
+
beforeGetList: async (params) => {
|
|
408
|
+
return applyFullTextSearch(["text", "contact_first_name"])(params);
|
|
409
|
+
},
|
|
410
|
+
},
|
|
374
411
|
{
|
|
375
412
|
resource: "deals",
|
|
376
413
|
beforeGetList: async (params) => {
|
|
@@ -27,6 +27,7 @@ import sales from "../sales";
|
|
|
27
27
|
import { DatabasePage } from "../settings/DatabasePage";
|
|
28
28
|
import { SettingsPage } from "../settings/SettingsPage";
|
|
29
29
|
import { IntegrationsPage } from "../integrations/IntegrationsPage";
|
|
30
|
+
import tasks from "../tasks";
|
|
30
31
|
import type { ConfigurationContextValue } from "./ConfigurationContext";
|
|
31
32
|
import { ConfigurationProvider } from "./ConfigurationContext";
|
|
32
33
|
import {
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
defaultDealStages,
|
|
39
40
|
defaultLightModeLogo,
|
|
40
41
|
defaultNoteStatuses,
|
|
42
|
+
defaultTaskPriorities,
|
|
43
|
+
defaultTaskStatuses,
|
|
41
44
|
defaultTaskTypes,
|
|
42
45
|
defaultTitle,
|
|
43
46
|
} from "./defaultConfiguration";
|
|
@@ -68,6 +71,8 @@ export type CRMProps = {
|
|
|
68
71
|
* @param {string} logo - The logo used in the CRM application.
|
|
69
72
|
* @param {NoteStatus[]} noteStatuses - The statuses of notes used in the application.
|
|
70
73
|
* @param {string[]} taskTypes - The types of tasks used in the application.
|
|
74
|
+
* @param {Object[]} taskPriorities - The priorities of tasks used in the application.
|
|
75
|
+
* @param {Object[]} taskStatuses - The statuses of tasks used in the application.
|
|
71
76
|
* @param {string} title - The title of the CRM application.
|
|
72
77
|
*
|
|
73
78
|
* @returns {JSX.Element} The rendered CRM application.
|
|
@@ -101,6 +106,8 @@ export const CRM = ({
|
|
|
101
106
|
lightModeLogo = defaultLightModeLogo,
|
|
102
107
|
noteStatuses = defaultNoteStatuses,
|
|
103
108
|
taskTypes = defaultTaskTypes,
|
|
109
|
+
taskPriorities = defaultTaskPriorities,
|
|
110
|
+
taskStatuses = defaultTaskStatuses,
|
|
104
111
|
title = defaultTitle,
|
|
105
112
|
dataProvider = defaultDataProvider,
|
|
106
113
|
authProvider = defaultAuthProvider,
|
|
@@ -132,6 +139,8 @@ export const CRM = ({
|
|
|
132
139
|
lightModeLogo={lightModeLogo}
|
|
133
140
|
noteStatuses={noteStatuses}
|
|
134
141
|
taskTypes={taskTypes}
|
|
142
|
+
taskPriorities={taskPriorities}
|
|
143
|
+
taskStatuses={taskStatuses}
|
|
135
144
|
title={title}
|
|
136
145
|
>
|
|
137
146
|
<DatabaseHealthCheck dataProvider={dataProvider}>
|
|
@@ -172,7 +181,9 @@ export const CRM = ({
|
|
|
172
181
|
<Resource name="companyNotes" />
|
|
173
182
|
<Resource name="contactNotes" />
|
|
174
183
|
<Resource name="dealNotes" />
|
|
175
|
-
<Resource name="
|
|
184
|
+
<Resource name="taskNotes" />
|
|
185
|
+
<Resource name="task_activity" />
|
|
186
|
+
<Resource name="tasks" {...tasks} />
|
|
176
187
|
<Resource name="sales" {...sales} />
|
|
177
188
|
<Resource name="tags" />
|
|
178
189
|
</Admin>
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
defaultDealStages,
|
|
11
11
|
defaultLightModeLogo,
|
|
12
12
|
defaultNoteStatuses,
|
|
13
|
+
defaultTaskPriorities,
|
|
14
|
+
defaultTaskStatuses,
|
|
13
15
|
defaultTaskTypes,
|
|
14
16
|
defaultTitle,
|
|
15
17
|
} from "./defaultConfiguration";
|
|
@@ -22,6 +24,8 @@ export interface ConfigurationContextValue {
|
|
|
22
24
|
dealStages: DealStage[];
|
|
23
25
|
noteStatuses: NoteStatus[];
|
|
24
26
|
taskTypes: string[];
|
|
27
|
+
taskPriorities: { id: string; name: string }[];
|
|
28
|
+
taskStatuses: { id: string; name: string }[];
|
|
25
29
|
title: string;
|
|
26
30
|
darkModeLogo: string;
|
|
27
31
|
lightModeLogo: string;
|
|
@@ -40,6 +44,8 @@ export const ConfigurationContext = createContext<ConfigurationContextValue>({
|
|
|
40
44
|
dealStages: defaultDealStages,
|
|
41
45
|
noteStatuses: defaultNoteStatuses,
|
|
42
46
|
taskTypes: defaultTaskTypes,
|
|
47
|
+
taskPriorities: defaultTaskPriorities,
|
|
48
|
+
taskStatuses: defaultTaskStatuses,
|
|
43
49
|
title: defaultTitle,
|
|
44
50
|
darkModeLogo: defaultDarkModeLogo,
|
|
45
51
|
lightModeLogo: defaultLightModeLogo,
|
|
@@ -56,6 +62,8 @@ export const ConfigurationProvider = ({
|
|
|
56
62
|
lightModeLogo,
|
|
57
63
|
noteStatuses,
|
|
58
64
|
taskTypes,
|
|
65
|
+
taskPriorities,
|
|
66
|
+
taskStatuses,
|
|
59
67
|
title,
|
|
60
68
|
contactGender,
|
|
61
69
|
}: ConfigurationProviderProps) => (
|
|
@@ -70,6 +78,8 @@ export const ConfigurationProvider = ({
|
|
|
70
78
|
noteStatuses,
|
|
71
79
|
title,
|
|
72
80
|
taskTypes,
|
|
81
|
+
taskPriorities,
|
|
82
|
+
taskStatuses,
|
|
73
83
|
contactGender,
|
|
74
84
|
}}
|
|
75
85
|
>
|
|
@@ -57,6 +57,21 @@ export const defaultTaskTypes = [
|
|
|
57
57
|
"Call",
|
|
58
58
|
];
|
|
59
59
|
|
|
60
|
+
export const defaultTaskPriorities = [
|
|
61
|
+
{ id: "low", name: "Low" },
|
|
62
|
+
{ id: "medium", name: "Medium" },
|
|
63
|
+
{ id: "high", name: "High" },
|
|
64
|
+
{ id: "urgent", name: "Urgent" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const defaultTaskStatuses = [
|
|
68
|
+
{ id: "todo", name: "To Do" },
|
|
69
|
+
{ id: "in_progress", name: "In Progress" },
|
|
70
|
+
{ id: "blocked", name: "Blocked" },
|
|
71
|
+
{ id: "done", name: "Done" },
|
|
72
|
+
{ id: "cancelled", name: "Cancelled" },
|
|
73
|
+
];
|
|
74
|
+
|
|
60
75
|
export const defaultContactGender = [
|
|
61
76
|
{ value: "male", label: "He/Him", icon: Mars },
|
|
62
77
|
{ value: "female", label: "She/Her", icon: Venus },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useGetIdentity, useListFilterContext } from "ra-core";
|
|
2
|
+
import { Label } from "@/components/ui/label";
|
|
3
|
+
import { Switch } from "@/components/ui/switch";
|
|
4
|
+
|
|
5
|
+
export const MyTasksInput = ({ source = "assigned_to", label = "My Tasks" }: { alwaysOn?: boolean; source?: string, label?: string }) => {
|
|
6
|
+
const { filterValues, displayedFilters, setFilters } = useListFilterContext();
|
|
7
|
+
const { identity } = useGetIdentity();
|
|
8
|
+
|
|
9
|
+
const handleChange = () => {
|
|
10
|
+
const newFilterValues = { ...filterValues };
|
|
11
|
+
if (typeof filterValues[source] !== "undefined") {
|
|
12
|
+
delete newFilterValues[source];
|
|
13
|
+
} else {
|
|
14
|
+
newFilterValues[source] = identity && identity?.id;
|
|
15
|
+
}
|
|
16
|
+
setFilters(newFilterValues, displayedFilters);
|
|
17
|
+
};
|
|
18
|
+
return (
|
|
19
|
+
<div className="mt-auto pb-2.25">
|
|
20
|
+
<div className="flex items-center space-x-2">
|
|
21
|
+
<Switch
|
|
22
|
+
id="my-tasks"
|
|
23
|
+
checked={typeof filterValues[source] !== "undefined"}
|
|
24
|
+
onCheckedChange={handleChange}
|
|
25
|
+
/>
|
|
26
|
+
<Label htmlFor="my-tasks">{label}</Label>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -2,6 +2,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
2
2
|
import { MoreVertical } from "lucide-react";
|
|
3
3
|
import { useDeleteWithUndoController, useNotify, useUpdate } from "ra-core";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
import { Link } from "react-router";
|
|
5
6
|
import { ReferenceField } from "@/components/admin/reference-field";
|
|
6
7
|
import { DateField } from "@/components/admin/date-field";
|
|
7
8
|
import { Button } from "@/components/ui/button";
|
|
@@ -85,15 +86,20 @@ export const Task = ({
|
|
|
85
86
|
className="mt-1"
|
|
86
87
|
/>
|
|
87
88
|
<div className={`flex-grow ${task.done_date ? "line-through" : ""}`}>
|
|
88
|
-
<
|
|
89
|
-
{task.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
<Link
|
|
90
|
+
to={`/tasks/${task.id}/show`}
|
|
91
|
+
className="block hover:text-primary transition-colors"
|
|
92
|
+
>
|
|
93
|
+
<div className="text-sm">
|
|
94
|
+
{task.type && task.type !== "None" && (
|
|
95
|
+
<>
|
|
96
|
+
<span className="font-semibold text-sm">{task.type}</span>
|
|
97
|
+
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
{task.text}
|
|
101
|
+
</div>
|
|
102
|
+
</Link>
|
|
97
103
|
<div className="text-sm text-muted-foreground">
|
|
98
104
|
due
|
|
99
105
|
<DateField source="due_date" record={task} />
|
|
@@ -133,6 +139,11 @@ export const Task = ({
|
|
|
133
139
|
</Button>
|
|
134
140
|
</DropdownMenuTrigger>
|
|
135
141
|
<DropdownMenuContent align="end">
|
|
142
|
+
<DropdownMenuItem asChild>
|
|
143
|
+
<Link to={`/tasks/${task.id}/show`} className="cursor-pointer">
|
|
144
|
+
Show Details
|
|
145
|
+
</Link>
|
|
146
|
+
</DropdownMenuItem>
|
|
136
147
|
<DropdownMenuItem
|
|
137
148
|
className="cursor-pointer"
|
|
138
149
|
onClick={() => {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { formatDistance } from "date-fns";
|
|
2
|
+
import { useGetList, useRecordContext } from "ra-core";
|
|
3
|
+
import { ReferenceField } from "@/components/admin/reference-field";
|
|
4
|
+
import type { TaskActivity, Sale } from "../types";
|
|
5
|
+
|
|
6
|
+
export const TaskActivityTimeline = ({
|
|
7
|
+
taskId,
|
|
8
|
+
}: {
|
|
9
|
+
taskId: string | number;
|
|
10
|
+
}) => {
|
|
11
|
+
const { data: activities, isPending } = useGetList<TaskActivity>(
|
|
12
|
+
"task_activity",
|
|
13
|
+
{
|
|
14
|
+
filter: { task_id: taskId },
|
|
15
|
+
pagination: { page: 1, perPage: 100 },
|
|
16
|
+
sort: { field: "created_at", order: "DESC" },
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (isPending)
|
|
21
|
+
return (
|
|
22
|
+
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
23
|
+
Loading activity...
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
if (!activities || activities.length === 0)
|
|
27
|
+
return (
|
|
28
|
+
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
29
|
+
No activity recorded.
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-6 relative pl-4 border-l border-border ml-2 my-4">
|
|
35
|
+
{activities.map((activity) => (
|
|
36
|
+
<div key={activity.id} className="relative">
|
|
37
|
+
<div className="absolute -left-[21px] top-1 h-3 w-3 rounded-full bg-primary ring-4 ring-background" />
|
|
38
|
+
<div className="text-sm">
|
|
39
|
+
<ReferenceField
|
|
40
|
+
source="sales_id"
|
|
41
|
+
record={activity}
|
|
42
|
+
reference="sales"
|
|
43
|
+
link={false}
|
|
44
|
+
>
|
|
45
|
+
<SaleName />
|
|
46
|
+
</ReferenceField>{" "}
|
|
47
|
+
{formatActivityMessage(activity)}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="text-xs text-muted-foreground">
|
|
50
|
+
{formatDistance(new Date(activity.created_at), new Date(), {
|
|
51
|
+
addSuffix: true,
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SaleName = () => {
|
|
61
|
+
const record = useRecordContext<Sale>();
|
|
62
|
+
if (!record) return null;
|
|
63
|
+
return (
|
|
64
|
+
<span className="font-semibold">
|
|
65
|
+
{record.first_name} {record.last_name}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const formatActivityMessage = (activity: TaskActivity) => {
|
|
71
|
+
switch (activity.action) {
|
|
72
|
+
case "created":
|
|
73
|
+
return "created this task";
|
|
74
|
+
case "updated":
|
|
75
|
+
if (!activity.field_name) return "updated this task";
|
|
76
|
+
return `changed ${activity.field_name}`;
|
|
77
|
+
// Simplified because old/new values might be IDs or technical values
|
|
78
|
+
case "assigned":
|
|
79
|
+
return "assigned this task";
|
|
80
|
+
case "completed":
|
|
81
|
+
return "completed this task";
|
|
82
|
+
case "reopened":
|
|
83
|
+
return "reopened this task";
|
|
84
|
+
case "duplicated":
|
|
85
|
+
return "duplicated this task";
|
|
86
|
+
case "archived":
|
|
87
|
+
return "archived this task";
|
|
88
|
+
default:
|
|
89
|
+
return activity.action;
|
|
90
|
+
}
|
|
91
|
+
};
|