realtimex-crm 0.13.7 → 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-B1VkO6yf.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/supabase/migrations/20251226120000_fix_ambiguous_column.sql +62 -0
- package/dist/assets/DealList-D4GG0g38.js +0 -59
- package/dist/assets/DealList-D4GG0g38.js.map +0 -1
- package/dist/assets/index-B1VkO6yf.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 }
|