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.
Files changed (47) hide show
  1. package/dist/assets/DealList-CRBXwi1I.js +59 -0
  2. package/dist/assets/DealList-CRBXwi1I.js.map +1 -0
  3. package/dist/assets/index-5AmasdLr.css +1 -0
  4. package/dist/assets/index-D5uatqhL.js +166 -0
  5. package/dist/assets/{index-BrW7DPxi.js.map → index-D5uatqhL.js.map} +1 -1
  6. package/dist/index.html +1 -1
  7. package/dist/stats.html +1 -1
  8. package/package.json +1 -1
  9. package/src/components/atomic-crm/deals/DealCreate.tsx +5 -1
  10. package/src/components/atomic-crm/deals/DealShow.tsx +5 -1
  11. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +10 -0
  12. package/src/components/atomic-crm/layout/Header.tsx +6 -0
  13. package/src/components/atomic-crm/notes/NoteCreate.tsx +3 -2
  14. package/src/components/atomic-crm/notes/NotesIterator.tsx +1 -1
  15. package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +4 -0
  16. package/src/components/atomic-crm/providers/fakerest/dataGenerator/taskActivity.ts +62 -0
  17. package/src/components/atomic-crm/providers/fakerest/dataGenerator/taskNotes.ts +29 -0
  18. package/src/components/atomic-crm/providers/fakerest/dataGenerator/tasks.ts +33 -8
  19. package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +4 -0
  20. package/src/components/atomic-crm/providers/supabase/dataProvider.ts +37 -0
  21. package/src/components/atomic-crm/root/CRM.tsx +12 -1
  22. package/src/components/atomic-crm/root/ConfigurationContext.tsx +10 -0
  23. package/src/components/atomic-crm/root/defaultConfiguration.ts +15 -0
  24. package/src/components/atomic-crm/tasks/MyTasksInput.tsx +30 -0
  25. package/src/components/atomic-crm/tasks/Task.tsx +20 -9
  26. package/src/components/atomic-crm/tasks/TaskActivityTimeline.tsx +91 -0
  27. package/src/components/atomic-crm/tasks/TaskAside.tsx +122 -0
  28. package/src/components/atomic-crm/tasks/TaskCreate.tsx +112 -0
  29. package/src/components/atomic-crm/tasks/TaskEdit.tsx +20 -1
  30. package/src/components/atomic-crm/tasks/TaskList.tsx +52 -0
  31. package/src/components/atomic-crm/tasks/TaskListTable.tsx +60 -0
  32. package/src/components/atomic-crm/tasks/TaskPriorityBadge.tsx +20 -0
  33. package/src/components/atomic-crm/tasks/TaskShow.tsx +71 -0
  34. package/src/components/atomic-crm/tasks/TaskStatusBadge.tsx +21 -0
  35. package/src/components/atomic-crm/tasks/index.ts +9 -0
  36. package/src/components/atomic-crm/types.ts +50 -0
  37. package/src/components/ui/visually-hidden.tsx +10 -0
  38. package/supabase/migrations/20251225085142_enhance_companies.sql +3 -3
  39. package/supabase/migrations/{20251226120000_fix_ambiguous_column.sql → 20251225110000_fix_ambiguous_column.sql} +2 -0
  40. package/supabase/migrations/20251225120000_enhance_tasks_schema.sql +111 -0
  41. package/supabase/migrations/20251225120001_enhance_tasks_logic.sql +109 -0
  42. package/supabase/migrations/20251225120002_enhance_tasks_webhooks.sql +72 -0
  43. package/supabase/migrations/20251225150000_add_taskNotes_attachments.sql +6 -0
  44. package/dist/assets/DealList-CyjZCmZS.js +0 -59
  45. package/dist/assets/DealList-CyjZCmZS.js.map +0 -1
  46. package/dist/assets/index-BrW7DPxi.js +0 -166
  47. 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.13.8",
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<Task>((id) => {
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: randomDate(
48
- datatype.boolean() ? new Date() : new Date(contact.first_seen),
49
- new Date(Date.now() + 100 * 24 * 60 * 60 * 1000),
50
- ).toISOString(),
51
- done_date: undefined,
52
- sales_id: 0,
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="tasks" />
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
- <div className="text-sm">
89
- {task.type && task.type !== "None" && (
90
- <>
91
- <span className="font-semibold text-sm">{task.type}</span>
92
- &nbsp;
93
- </>
94
- )}
95
- {task.text}
96
- </div>
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
+ &nbsp;
98
+ </>
99
+ )}
100
+ {task.text}
101
+ </div>
102
+ </Link>
97
103
  <div className="text-sm text-muted-foreground">
98
104
  due&nbsp;
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
+ };