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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Calendar, Building2, UserCircle, UserCheck, Pencil } from "lucide-react";
|
|
2
|
+
import { useRecordContext } from "ra-core";
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { DeleteButton } from "@/components/admin/delete-button";
|
|
6
|
+
import { ReferenceField } from "@/components/admin/reference-field";
|
|
7
|
+
import { TextField } from "@/components/admin/text-field";
|
|
8
|
+
import { DateField } from "@/components/admin/date-field";
|
|
9
|
+
|
|
10
|
+
import { AsideSection } from "../misc/AsideSection";
|
|
11
|
+
import type { Task } from "../types";
|
|
12
|
+
import { TaskEdit } from "./TaskEdit";
|
|
13
|
+
|
|
14
|
+
export const TaskAside = () => {
|
|
15
|
+
const record = useRecordContext<Task>();
|
|
16
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
if (!record) return null;
|
|
19
|
+
return (
|
|
20
|
+
<div className="hidden sm:block w-64 min-w-64 text-sm">
|
|
21
|
+
<div className="mb-4 -ml-1">
|
|
22
|
+
<Button
|
|
23
|
+
variant="outline"
|
|
24
|
+
size="sm"
|
|
25
|
+
onClick={() => setEditOpen(true)}
|
|
26
|
+
className="flex items-center gap-2"
|
|
27
|
+
>
|
|
28
|
+
<Pencil className="h-4 w-4" />
|
|
29
|
+
Edit Task
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<TaskEdit
|
|
34
|
+
open={editOpen}
|
|
35
|
+
close={() => setEditOpen(false)}
|
|
36
|
+
taskId={record.id}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<AsideSection title="Task Info">
|
|
40
|
+
<InfoRow
|
|
41
|
+
icon={<Calendar className="w-4 h-4 text-muted-foreground" />}
|
|
42
|
+
label="Due Date"
|
|
43
|
+
value={<DateField source="due_date" />}
|
|
44
|
+
/>
|
|
45
|
+
{record.done_date && (
|
|
46
|
+
<InfoRow
|
|
47
|
+
icon={<Calendar className="w-4 h-4 text-muted-foreground" />}
|
|
48
|
+
label="Completed"
|
|
49
|
+
value={<DateField source="done_date" />}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</AsideSection>
|
|
53
|
+
|
|
54
|
+
<AsideSection title="Related">
|
|
55
|
+
<InfoRow
|
|
56
|
+
icon={<UserCircle className="w-4 h-4 text-muted-foreground" />}
|
|
57
|
+
label="Contact"
|
|
58
|
+
value={
|
|
59
|
+
<ReferenceField
|
|
60
|
+
source="contact_id"
|
|
61
|
+
reference="contacts"
|
|
62
|
+
link="show"
|
|
63
|
+
/>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
<InfoRow
|
|
67
|
+
icon={<Building2 className="w-4 h-4 text-muted-foreground" />}
|
|
68
|
+
label="Company"
|
|
69
|
+
value={
|
|
70
|
+
<ReferenceField source="contact_id" reference="contacts" link={false}>
|
|
71
|
+
<ReferenceField source="company_id" reference="companies" link="show">
|
|
72
|
+
<TextField source="name" />
|
|
73
|
+
</ReferenceField>
|
|
74
|
+
</ReferenceField>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
</AsideSection>
|
|
78
|
+
|
|
79
|
+
<AsideSection title="Assignment">
|
|
80
|
+
<InfoRow
|
|
81
|
+
icon={<UserCheck className="w-4 h-4 text-muted-foreground" />}
|
|
82
|
+
label="Assigned To"
|
|
83
|
+
value={
|
|
84
|
+
<ReferenceField source="assigned_to" reference="sales" link={false} />
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
<InfoRow
|
|
88
|
+
icon={<UserCircle className="w-4 h-4 text-muted-foreground" />}
|
|
89
|
+
label="Created By"
|
|
90
|
+
value={
|
|
91
|
+
<ReferenceField source="sales_id" reference="sales" link={false} />
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
</AsideSection>
|
|
95
|
+
|
|
96
|
+
<div className="mt-6 pt-6 border-t hidden sm:flex flex-col gap-2 items-start">
|
|
97
|
+
<DeleteButton
|
|
98
|
+
className="h-6 cursor-pointer hover:bg-destructive/10! text-destructive! border-destructive! focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40"
|
|
99
|
+
size="sm"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const InfoRow = ({
|
|
107
|
+
icon,
|
|
108
|
+
label,
|
|
109
|
+
value,
|
|
110
|
+
}: {
|
|
111
|
+
icon: ReactNode;
|
|
112
|
+
label: string;
|
|
113
|
+
value: ReactNode;
|
|
114
|
+
}) => (
|
|
115
|
+
<div className="flex flex-col gap-1 mb-3">
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
{icon}
|
|
118
|
+
<span className="text-xs text-muted-foreground">{label}</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="pl-6 text-sm">{value}</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AutocompleteInput } from "@/components/admin/autocomplete-input";
|
|
2
|
+
import { DateInput } from "@/components/admin/date-input";
|
|
3
|
+
import { ReferenceInput } from "@/components/admin/reference-input";
|
|
4
|
+
import { SelectInput } from "@/components/admin/select-input";
|
|
5
|
+
import { TextInput } from "@/components/admin/text-input";
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import { Create } from "@/components/admin/create";
|
|
8
|
+
import {
|
|
9
|
+
Form,
|
|
10
|
+
required,
|
|
11
|
+
useGetIdentity,
|
|
12
|
+
useNotify,
|
|
13
|
+
useRedirect,
|
|
14
|
+
} from "ra-core";
|
|
15
|
+
|
|
16
|
+
import { FormToolbar } from "../layout/FormToolbar";
|
|
17
|
+
import { contactOptionText } from "../misc/ContactOption";
|
|
18
|
+
import { useConfigurationContext } from "../root/ConfigurationContext";
|
|
19
|
+
|
|
20
|
+
export const TaskCreate = () => {
|
|
21
|
+
const { identity } = useGetIdentity();
|
|
22
|
+
const { taskTypes, taskPriorities, taskStatuses } = useConfigurationContext();
|
|
23
|
+
const notify = useNotify();
|
|
24
|
+
const redirect = useRedirect();
|
|
25
|
+
|
|
26
|
+
const handleSuccess = () => {
|
|
27
|
+
notify("Task created");
|
|
28
|
+
redirect("list", "tasks");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="mt-4 max-w-2xl mx-auto">
|
|
33
|
+
<Card>
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<CardTitle>Create Task</CardTitle>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<Create
|
|
39
|
+
resource="tasks"
|
|
40
|
+
redirect="list"
|
|
41
|
+
mutationOptions={{ onSuccess: handleSuccess }}
|
|
42
|
+
transform={(data) => ({
|
|
43
|
+
...data,
|
|
44
|
+
sales_id: identity?.id,
|
|
45
|
+
created_at: new Date().toISOString(),
|
|
46
|
+
updated_at: new Date().toISOString(),
|
|
47
|
+
})}
|
|
48
|
+
>
|
|
49
|
+
<Form
|
|
50
|
+
defaultValues={{
|
|
51
|
+
due_date: new Date().toISOString().slice(0, 10),
|
|
52
|
+
priority: "medium",
|
|
53
|
+
status: "todo",
|
|
54
|
+
assigned_to: identity?.id,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<TextInput
|
|
58
|
+
autoFocus
|
|
59
|
+
source="text"
|
|
60
|
+
label="Description"
|
|
61
|
+
validate={required()}
|
|
62
|
+
multiline
|
|
63
|
+
className="w-full"
|
|
64
|
+
/>
|
|
65
|
+
<div className="grid grid-cols-2 gap-4 mt-4">
|
|
66
|
+
<ReferenceInput
|
|
67
|
+
source="contact_id"
|
|
68
|
+
reference="contacts_summary"
|
|
69
|
+
>
|
|
70
|
+
<AutocompleteInput
|
|
71
|
+
label="Contact"
|
|
72
|
+
optionText={contactOptionText}
|
|
73
|
+
validate={required()}
|
|
74
|
+
/>
|
|
75
|
+
</ReferenceInput>
|
|
76
|
+
<DateInput
|
|
77
|
+
source="due_date"
|
|
78
|
+
validate={required()}
|
|
79
|
+
/>
|
|
80
|
+
<SelectInput
|
|
81
|
+
source="type"
|
|
82
|
+
validate={required()}
|
|
83
|
+
choices={taskTypes.map((type) => ({
|
|
84
|
+
id: type,
|
|
85
|
+
name: type,
|
|
86
|
+
}))}
|
|
87
|
+
/>
|
|
88
|
+
<SelectInput
|
|
89
|
+
source="priority"
|
|
90
|
+
choices={taskPriorities}
|
|
91
|
+
/>
|
|
92
|
+
<SelectInput
|
|
93
|
+
source="status"
|
|
94
|
+
choices={taskStatuses}
|
|
95
|
+
/>
|
|
96
|
+
<ReferenceInput source="assigned_to" reference="sales">
|
|
97
|
+
<SelectInput
|
|
98
|
+
optionText={(record) =>
|
|
99
|
+
`${record.first_name} ${record.last_name}`
|
|
100
|
+
}
|
|
101
|
+
label="Assigned To"
|
|
102
|
+
/>
|
|
103
|
+
</ReferenceInput>
|
|
104
|
+
</div>
|
|
105
|
+
<FormToolbar />
|
|
106
|
+
</Form>
|
|
107
|
+
</Create>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -3,6 +3,7 @@ import { DeleteButton } from "@/components/admin/delete-button";
|
|
|
3
3
|
import { TextInput } from "@/components/admin/text-input";
|
|
4
4
|
import { DateInput } from "@/components/admin/date-input";
|
|
5
5
|
import { SelectInput } from "@/components/admin/select-input";
|
|
6
|
+
import { ReferenceInput } from "@/components/admin/reference-input";
|
|
6
7
|
import { SaveButton } from "@/components/admin/form";
|
|
7
8
|
import {
|
|
8
9
|
Dialog,
|
|
@@ -23,7 +24,7 @@ export const TaskEdit = ({
|
|
|
23
24
|
open: boolean;
|
|
24
25
|
close: () => void;
|
|
25
26
|
}) => {
|
|
26
|
-
const { taskTypes } = useConfigurationContext();
|
|
27
|
+
const { taskTypes, taskPriorities, taskStatuses } = useConfigurationContext();
|
|
27
28
|
const notify = useNotify();
|
|
28
29
|
return (
|
|
29
30
|
<Dialog open={open} onOpenChange={close}>
|
|
@@ -71,6 +72,24 @@ export const TaskEdit = ({
|
|
|
71
72
|
helperText={false}
|
|
72
73
|
validate={required()}
|
|
73
74
|
/>
|
|
75
|
+
<SelectInput
|
|
76
|
+
source="priority"
|
|
77
|
+
choices={taskPriorities}
|
|
78
|
+
helperText={false}
|
|
79
|
+
/>
|
|
80
|
+
<SelectInput
|
|
81
|
+
source="status"
|
|
82
|
+
choices={taskStatuses}
|
|
83
|
+
helperText={false}
|
|
84
|
+
/>
|
|
85
|
+
<ReferenceInput source="assigned_to" reference="sales">
|
|
86
|
+
<SelectInput
|
|
87
|
+
optionText={(record) =>
|
|
88
|
+
`${record.first_name} ${record.last_name}`
|
|
89
|
+
}
|
|
90
|
+
label="Assigned To"
|
|
91
|
+
/>
|
|
92
|
+
</ReferenceInput>
|
|
74
93
|
</div>
|
|
75
94
|
<DialogFooter className="w-full sm:justify-between gap-4">
|
|
76
95
|
<DeleteButton
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AutocompleteInput } from "@/components/admin/autocomplete-input";
|
|
2
|
+
import { BooleanInput } from "@/components/admin/boolean-input";
|
|
3
|
+
import { CreateButton } from "@/components/admin/create-button";
|
|
4
|
+
import { ExportButton } from "@/components/admin/export-button";
|
|
5
|
+
import { FilterButton } from "@/components/admin/filter-form";
|
|
6
|
+
import { List } from "@/components/admin/list";
|
|
7
|
+
import { ReferenceInput } from "@/components/admin/reference-input";
|
|
8
|
+
import { SearchInput } from "@/components/admin/search-input";
|
|
9
|
+
import { SelectInput } from "@/components/admin/select-input";
|
|
10
|
+
|
|
11
|
+
import { useConfigurationContext } from "../root/ConfigurationContext";
|
|
12
|
+
import { TopToolbar } from "../layout/TopToolbar";
|
|
13
|
+
import { MyTasksInput } from "./MyTasksInput";
|
|
14
|
+
import { TaskListTable } from "./TaskListTable";
|
|
15
|
+
|
|
16
|
+
const TaskList = () => {
|
|
17
|
+
const { taskStatuses, taskPriorities } = useConfigurationContext();
|
|
18
|
+
|
|
19
|
+
const taskFilters = [
|
|
20
|
+
<SearchInput source="q" alwaysOn />,
|
|
21
|
+
<SelectInput source="status" choices={taskStatuses} alwaysOn />,
|
|
22
|
+
<ReferenceInput source="contact_id" reference="contacts">
|
|
23
|
+
<AutocompleteInput label={false} placeholder="Contact" />
|
|
24
|
+
</ReferenceInput>,
|
|
25
|
+
<SelectInput source="priority" choices={taskPriorities} />,
|
|
26
|
+
<MyTasksInput source="assigned_to" label="My Tasks" alwaysOn />,
|
|
27
|
+
<BooleanInput source="archived" label="Archived" />,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<List
|
|
32
|
+
perPage={25}
|
|
33
|
+
sort={{ field: "due_date", order: "ASC" }}
|
|
34
|
+
filters={taskFilters}
|
|
35
|
+
filterDefaultValues={{ archived: false }}
|
|
36
|
+
actions={<TaskActions />}
|
|
37
|
+
title="Tasks"
|
|
38
|
+
>
|
|
39
|
+
<TaskListTable />
|
|
40
|
+
</List>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const TaskActions = () => (
|
|
45
|
+
<TopToolbar>
|
|
46
|
+
<FilterButton />
|
|
47
|
+
<ExportButton />
|
|
48
|
+
<CreateButton label="New Task" />
|
|
49
|
+
</TopToolbar>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export default TaskList;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DataTable } from "@/components/admin/data-table";
|
|
2
|
+
import { DateField } from "@/components/admin/date-field";
|
|
3
|
+
import { ReferenceField } from "@/components/admin/reference-field";
|
|
4
|
+
import { TextField } from "@/components/admin/text-field";
|
|
5
|
+
|
|
6
|
+
import type { Task } from "../types";
|
|
7
|
+
import { TaskPriorityBadge } from "./TaskPriorityBadge";
|
|
8
|
+
import { TaskStatusBadge } from "./TaskStatusBadge";
|
|
9
|
+
|
|
10
|
+
export const TaskListTable = () => {
|
|
11
|
+
return (
|
|
12
|
+
<DataTable rowClick="show">
|
|
13
|
+
<DataTable.Col
|
|
14
|
+
source="text"
|
|
15
|
+
label="Task"
|
|
16
|
+
className="w-[35%]"
|
|
17
|
+
cellClassName="max-w-md"
|
|
18
|
+
render={(record: Task) => (
|
|
19
|
+
<div className="truncate" title={record.text}>
|
|
20
|
+
{record.type && record.type !== "None" && (
|
|
21
|
+
<span className="font-semibold">{record.type}: </span>
|
|
22
|
+
)}
|
|
23
|
+
{record.text}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
<DataTable.Col label="Contact" className="w-[15%]">
|
|
28
|
+
<ReferenceField source="contact_id" reference="contacts" link="show" />
|
|
29
|
+
</DataTable.Col>
|
|
30
|
+
<DataTable.Col label="Company" className="w-[15%]">
|
|
31
|
+
<ReferenceField
|
|
32
|
+
source="company_id"
|
|
33
|
+
reference="companies"
|
|
34
|
+
link="show"
|
|
35
|
+
sortable={false}
|
|
36
|
+
>
|
|
37
|
+
<TextField source="name" />
|
|
38
|
+
</ReferenceField>
|
|
39
|
+
</DataTable.Col>
|
|
40
|
+
<DataTable.Col label="Due Date" className="w-[12%]">
|
|
41
|
+
<DateField source="due_date" />
|
|
42
|
+
</DataTable.Col>
|
|
43
|
+
<DataTable.Col
|
|
44
|
+
label="Priority"
|
|
45
|
+
className="w-[10%]"
|
|
46
|
+
render={(record: Task) => (
|
|
47
|
+
<TaskPriorityBadge priority={record.priority} />
|
|
48
|
+
)}
|
|
49
|
+
/>
|
|
50
|
+
<DataTable.Col
|
|
51
|
+
label="Status"
|
|
52
|
+
className="w-[10%]"
|
|
53
|
+
render={(record: Task) => <TaskStatusBadge status={record.status} />}
|
|
54
|
+
/>
|
|
55
|
+
<DataTable.Col label="Assigned To" className="w-[13%]">
|
|
56
|
+
<ReferenceField source="assigned_to" reference="sales" link={false} />
|
|
57
|
+
</DataTable.Col>
|
|
58
|
+
</DataTable>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Badge } from "@/components/ui/badge";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const priorityColors = {
|
|
5
|
+
low: "bg-slate-500 hover:bg-slate-600",
|
|
6
|
+
medium: "bg-blue-500 hover:bg-blue-600",
|
|
7
|
+
high: "bg-orange-500 hover:bg-orange-600",
|
|
8
|
+
urgent: "bg-red-500 hover:bg-red-600",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const TaskPriorityBadge = ({ priority }: { priority?: string }) => {
|
|
12
|
+
if (!priority) return null;
|
|
13
|
+
const colorClass = priorityColors[priority as keyof typeof priorityColors] || "bg-slate-500";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Badge className={cn("capitalize", colorClass)}>
|
|
17
|
+
{priority}
|
|
18
|
+
</Badge>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ShowBase, useShowContext } from "ra-core";
|
|
2
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { ReferenceManyField } from "@/components/admin/reference-many-field";
|
|
4
|
+
|
|
5
|
+
import { NoteCreate, NotesIterator } from "../notes";
|
|
6
|
+
import type { Task } from "../types";
|
|
7
|
+
import { TaskAside } from "./TaskAside";
|
|
8
|
+
import { TaskPriorityBadge } from "./TaskPriorityBadge";
|
|
9
|
+
import { TaskStatusBadge } from "./TaskStatusBadge";
|
|
10
|
+
import { TaskActivityTimeline } from "./TaskActivityTimeline";
|
|
11
|
+
|
|
12
|
+
export const TaskShow = () => (
|
|
13
|
+
<ShowBase>
|
|
14
|
+
<TaskShowContent />
|
|
15
|
+
</ShowBase>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const TaskShowContent = () => {
|
|
19
|
+
const { record, isPending } = useShowContext<Task>();
|
|
20
|
+
if (isPending || !record) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="mt-2 mb-2 flex gap-8">
|
|
24
|
+
<div className="flex-1">
|
|
25
|
+
<Card>
|
|
26
|
+
<CardContent>
|
|
27
|
+
{/* Task Header */}
|
|
28
|
+
<div className="mb-6">
|
|
29
|
+
<h5 className="text-xl font-semibold mb-3">{record.type}</h5>
|
|
30
|
+
<div className="flex gap-3 mb-4">
|
|
31
|
+
<TaskStatusBadge status={record.status} />
|
|
32
|
+
<TaskPriorityBadge priority={record.priority} />
|
|
33
|
+
</div>
|
|
34
|
+
{record.text && (
|
|
35
|
+
<p className="text-sm text-muted-foreground whitespace-pre-line">
|
|
36
|
+
{record.text}
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Activity Timeline */}
|
|
42
|
+
<div className="mt-8">
|
|
43
|
+
<h3 className="text-lg font-semibold mb-4">Activity Timeline</h3>
|
|
44
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
45
|
+
Track all status changes and updates for this task
|
|
46
|
+
</p>
|
|
47
|
+
<TaskActivityTimeline taskId={record.id} />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Notes */}
|
|
51
|
+
<div className="mt-8">
|
|
52
|
+
<h3 className="text-lg font-semibold mb-4">Notes</h3>
|
|
53
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
54
|
+
Add notes and updates to this task
|
|
55
|
+
</p>
|
|
56
|
+
<ReferenceManyField
|
|
57
|
+
reference="taskNotes"
|
|
58
|
+
target="task_id"
|
|
59
|
+
sort={{ field: "date", order: "DESC" }}
|
|
60
|
+
empty={<NoteCreate reference="tasks" showStatus className="mt-4" />}
|
|
61
|
+
>
|
|
62
|
+
<NotesIterator reference="tasks" showStatus />
|
|
63
|
+
</ReferenceManyField>
|
|
64
|
+
</div>
|
|
65
|
+
</CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
</div>
|
|
68
|
+
<TaskAside />
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Badge } from "@/components/ui/badge";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const statusColors = {
|
|
5
|
+
todo: "bg-slate-500 hover:bg-slate-600",
|
|
6
|
+
in_progress: "bg-blue-500 hover:bg-blue-600",
|
|
7
|
+
blocked: "bg-red-500 hover:bg-red-600",
|
|
8
|
+
done: "bg-green-500 hover:bg-green-600",
|
|
9
|
+
cancelled: "bg-gray-400 hover:bg-gray-500",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const TaskStatusBadge = ({ status }: { status?: string }) => {
|
|
13
|
+
if (!status) return null;
|
|
14
|
+
const colorClass = statusColors[status as keyof typeof statusColors] || "bg-slate-500";
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Badge className={cn("capitalize", colorClass)}>
|
|
18
|
+
{status.replace("_", " ")}
|
|
19
|
+
</Badge>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -216,6 +216,15 @@ export type Tag = {
|
|
|
216
216
|
color: string;
|
|
217
217
|
} & Pick<RaRecord, "id">;
|
|
218
218
|
|
|
219
|
+
export type TaskPriority = "low" | "medium" | "high" | "urgent";
|
|
220
|
+
|
|
221
|
+
export type TaskStatus =
|
|
222
|
+
| "todo"
|
|
223
|
+
| "in_progress"
|
|
224
|
+
| "blocked"
|
|
225
|
+
| "done"
|
|
226
|
+
| "cancelled";
|
|
227
|
+
|
|
219
228
|
export type Task = {
|
|
220
229
|
contact_id: Identifier;
|
|
221
230
|
type: string;
|
|
@@ -223,6 +232,47 @@ export type Task = {
|
|
|
223
232
|
due_date: string;
|
|
224
233
|
done_date?: string | null;
|
|
225
234
|
sales_id?: Identifier;
|
|
235
|
+
priority?: TaskPriority;
|
|
236
|
+
assigned_to?: Identifier;
|
|
237
|
+
status?: TaskStatus;
|
|
238
|
+
created_at?: string;
|
|
239
|
+
updated_at?: string;
|
|
240
|
+
archived?: boolean;
|
|
241
|
+
archived_at?: string;
|
|
242
|
+
} & Pick<RaRecord, "id">;
|
|
243
|
+
|
|
244
|
+
export type TaskSummary = Task & {
|
|
245
|
+
contact_first_name?: string;
|
|
246
|
+
contact_last_name?: string;
|
|
247
|
+
contact_email?: string;
|
|
248
|
+
company_id?: Identifier;
|
|
249
|
+
company_name?: string;
|
|
250
|
+
assigned_first_name?: string;
|
|
251
|
+
assigned_last_name?: string;
|
|
252
|
+
creator_first_name?: string;
|
|
253
|
+
creator_last_name?: string;
|
|
254
|
+
nb_notes?: number;
|
|
255
|
+
last_note_date?: string;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export type TaskNote = {
|
|
259
|
+
task_id: Identifier;
|
|
260
|
+
text: string;
|
|
261
|
+
date: string;
|
|
262
|
+
sales_id: Identifier;
|
|
263
|
+
status?: string;
|
|
264
|
+
created_at?: string;
|
|
265
|
+
updated_at?: string;
|
|
266
|
+
} & Pick<RaRecord, "id">;
|
|
267
|
+
|
|
268
|
+
export type TaskActivity = {
|
|
269
|
+
task_id: Identifier;
|
|
270
|
+
sales_id: Identifier;
|
|
271
|
+
action: string;
|
|
272
|
+
field_name?: string;
|
|
273
|
+
old_value?: string;
|
|
274
|
+
new_value?: string;
|
|
275
|
+
created_at: string;
|
|
226
276
|
} & Pick<RaRecord, "id">;
|
|
227
277
|
|
|
228
278
|
export type ActivityCompanyCreated = {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden"
|
|
3
|
+
|
|
4
|
+
function VisuallyHidden({
|
|
5
|
+
...props
|
|
6
|
+
}: React.ComponentProps<typeof VisuallyHiddenPrimitive.Root>) {
|
|
7
|
+
return <VisuallyHiddenPrimitive.Root data-slot="visually-hidden" {...props} />
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { VisuallyHidden }
|
|
@@ -110,7 +110,7 @@ CREATE TRIGGER set_internal_heartbeat_timestamp
|
|
|
110
110
|
-- Phase 5: Internal Heartbeat Computation Function
|
|
111
111
|
-- ============================================================================
|
|
112
112
|
|
|
113
|
-
CREATE OR REPLACE FUNCTION compute_company_internal_heartbeat(
|
|
113
|
+
CREATE OR REPLACE FUNCTION compute_company_internal_heartbeat(p_company_id bigint)
|
|
114
114
|
RETURNS void AS $$
|
|
115
115
|
DECLARE
|
|
116
116
|
score integer := 0;
|
|
@@ -131,7 +131,7 @@ BEGIN
|
|
|
131
131
|
LEFT JOIN deals d ON c.id = d.company_id
|
|
132
132
|
LEFT JOIN contacts co ON c.id = co.company_id
|
|
133
133
|
LEFT JOIN tasks t ON co.id = t.contact_id
|
|
134
|
-
WHERE c.id =
|
|
134
|
+
WHERE c.id = p_company_id;
|
|
135
135
|
|
|
136
136
|
-- Scoring algorithm (simple recency-based, 0-100 scale)
|
|
137
137
|
score := 100;
|
|
@@ -166,7 +166,7 @@ BEGIN
|
|
|
166
166
|
internal_heartbeat_score = score,
|
|
167
167
|
internal_heartbeat_status = status,
|
|
168
168
|
internal_heartbeat_updated_at = now()
|
|
169
|
-
WHERE id =
|
|
169
|
+
WHERE id = p_company_id;
|
|
170
170
|
END;
|
|
171
171
|
$$ LANGUAGE plpgsql;
|
|
172
172
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
-- Fix ambiguous column reference in compute_company_internal_heartbeat by renaming parameter
|
|
2
|
+
-- This fix is moved earlier in the migration sequence to ensure it's available
|
|
3
|
+
-- before the tasks enhancement migration runs its updates.
|
|
2
4
|
|
|
3
5
|
DROP FUNCTION IF EXISTS compute_company_internal_heartbeat(bigint);
|
|
4
6
|
|