realtimex-crm 0.11.2 → 0.12.2
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-B_Xs90US.js → DealList-BvoXZ_PI.js} +2 -2
- package/dist/assets/{DealList-B_Xs90US.js.map → DealList-BvoXZ_PI.js.map} +1 -1
- package/dist/assets/{index-B-3Bhhk5.js → index-D-i5w9_j.js} +54 -54
- package/dist/assets/{index-B-3Bhhk5.js.map → index-D-i5w9_j.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/activity/ActivityLogCompanyNoteCreated.tsx +60 -0
- package/src/components/atomic-crm/activity/ActivityLogIterator.tsx +6 -0
- package/src/components/atomic-crm/companies/CompanyListFilter.tsx +2 -0
- package/src/components/atomic-crm/companies/CompanyShow.tsx +20 -1
- package/src/components/atomic-crm/consts.ts +1 -0
- package/src/components/atomic-crm/dashboard/DealsChart.tsx +29 -6
- package/src/components/atomic-crm/notes/NoteCreate.tsx +10 -3
- package/src/components/atomic-crm/notes/NotesIterator.tsx +1 -1
- package/src/components/atomic-crm/providers/commons/activity.ts +38 -2
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/companyNotes.ts +20 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +2 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +2 -0
- package/src/components/atomic-crm/root/CRM.tsx +1 -0
- package/src/components/atomic-crm/types.ts +21 -0
- package/src/components/ui/input.tsx +10 -1
- package/src/components/ui/textarea.tsx +9 -1
- package/supabase/migrations/20251223185638_remove_large_payload_storage.sql +9 -0
- package/supabase/migrations/20251224000002_add_company_notes.sql +67 -0
- package/supabase/migrations/20251224000003_add_company_last_seen.sql +5 -0
- package/supabase/migrations/20251224000004_update_companies_summary_view.sql +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ReferenceField } from "@/components/admin/reference-field";
|
|
2
|
+
|
|
3
|
+
import { CompanyAvatar } from "../companies/CompanyAvatar";
|
|
4
|
+
import { RelativeDate } from "../misc/RelativeDate";
|
|
5
|
+
import { SaleName } from "../sales/SaleName";
|
|
6
|
+
import type { ActivityCompanyNoteCreated } from "../types";
|
|
7
|
+
import { useActivityLogContext } from "./ActivityLogContext";
|
|
8
|
+
import { ActivityLogNote } from "./ActivityLogNote";
|
|
9
|
+
|
|
10
|
+
type ActivityLogCompanyNoteCreatedProps = {
|
|
11
|
+
activity: ActivityCompanyNoteCreated;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function ActivityLogCompanyNoteCreated({
|
|
15
|
+
activity,
|
|
16
|
+
}: ActivityLogCompanyNoteCreatedProps) {
|
|
17
|
+
const context = useActivityLogContext();
|
|
18
|
+
const { companyNote } = activity;
|
|
19
|
+
return (
|
|
20
|
+
<ActivityLogNote
|
|
21
|
+
header={
|
|
22
|
+
<div className="flex flex-row items-center gap-2 flex-grow">
|
|
23
|
+
<ReferenceField
|
|
24
|
+
source="company_id"
|
|
25
|
+
reference="companies"
|
|
26
|
+
record={companyNote}
|
|
27
|
+
link={false}
|
|
28
|
+
>
|
|
29
|
+
<CompanyAvatar width={20} height={20} />
|
|
30
|
+
</ReferenceField>
|
|
31
|
+
|
|
32
|
+
<span className="text-sm text-muted-foreground flex-grow inline-flex">
|
|
33
|
+
<ReferenceField
|
|
34
|
+
source="sales_id"
|
|
35
|
+
reference="sales"
|
|
36
|
+
record={activity}
|
|
37
|
+
link={false}
|
|
38
|
+
>
|
|
39
|
+
<SaleName />
|
|
40
|
+
</ReferenceField>
|
|
41
|
+
added a note about
|
|
42
|
+
<ReferenceField
|
|
43
|
+
source="company_id"
|
|
44
|
+
reference="companies"
|
|
45
|
+
record={companyNote}
|
|
46
|
+
link="show"
|
|
47
|
+
/>
|
|
48
|
+
</span>
|
|
49
|
+
|
|
50
|
+
{context === "company" && (
|
|
51
|
+
<span className="text-muted-foreground text-sm">
|
|
52
|
+
<RelativeDate date={activity.date} />
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
57
|
+
text={companyNote.text}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Fragment, useState } from "react";
|
|
|
3
3
|
import { Separator } from "@/components/ui/separator";
|
|
4
4
|
import {
|
|
5
5
|
COMPANY_CREATED,
|
|
6
|
+
COMPANY_NOTE_CREATED,
|
|
6
7
|
CONTACT_CREATED,
|
|
7
8
|
CONTACT_NOTE_CREATED,
|
|
8
9
|
DEAL_CREATED,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from "../consts";
|
|
11
12
|
import type { Activity } from "../types";
|
|
12
13
|
import { ActivityLogCompanyCreated } from "./ActivityLogCompanyCreated";
|
|
14
|
+
import { ActivityLogCompanyNoteCreated } from "./ActivityLogCompanyNoteCreated";
|
|
13
15
|
import { ActivityLogContactCreated } from "./ActivityLogContactCreated";
|
|
14
16
|
import { ActivityLogContactNoteCreated } from "./ActivityLogContactNoteCreated";
|
|
15
17
|
import { ActivityLogDealCreated } from "./ActivityLogDealCreated";
|
|
@@ -76,5 +78,9 @@ function ActivityItem({ activity }: { activity: Activity }) {
|
|
|
76
78
|
return <ActivityLogDealNoteCreated activity={activity} />;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
if (activity.type === COMPANY_NOTE_CREATED) {
|
|
82
|
+
return <ActivityLogCompanyNoteCreated activity={activity} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
return null;
|
|
80
86
|
}
|
|
@@ -23,6 +23,7 @@ export const CompanyListFilter = () => {
|
|
|
23
23
|
<FilterCategory icon={<Building className="h-4 w-4" />} label="Size">
|
|
24
24
|
{sizes.map((size) => (
|
|
25
25
|
<ToggleFilterButton
|
|
26
|
+
key={size.id}
|
|
26
27
|
className="w-full justify-between"
|
|
27
28
|
label={size.name}
|
|
28
29
|
value={{ size: size.id }}
|
|
@@ -33,6 +34,7 @@ export const CompanyListFilter = () => {
|
|
|
33
34
|
<FilterCategory icon={<Truck className="h-4 w-4" />} label="Sector">
|
|
34
35
|
{sectors.map((sector) => (
|
|
35
36
|
<ToggleFilterButton
|
|
37
|
+
key={sector.id}
|
|
36
38
|
className="w-full justify-between"
|
|
37
39
|
label={sector.name}
|
|
38
40
|
value={{ sector: sector.id }}
|
|
@@ -24,6 +24,8 @@ import { Avatar } from "../contacts/Avatar";
|
|
|
24
24
|
import { TagsList } from "../contacts/TagsList";
|
|
25
25
|
import { findDealLabel } from "../deals/deal";
|
|
26
26
|
import { Status } from "../misc/Status";
|
|
27
|
+
import { NoteCreate } from "../notes/NoteCreate";
|
|
28
|
+
import { NotesIterator } from "../notes/NotesIterator";
|
|
27
29
|
import { useConfigurationContext } from "../root/ConfigurationContext";
|
|
28
30
|
import type { Company, Contact, Deal } from "../types";
|
|
29
31
|
import { CompanyAside } from "./CompanyAside";
|
|
@@ -64,7 +66,7 @@ const CompanyShowContent = () => {
|
|
|
64
66
|
<h5 className="text-xl ml-2 flex-1">{record.name}</h5>
|
|
65
67
|
</div>
|
|
66
68
|
<Tabs defaultValue={currentTab} onValueChange={handleTabChange}>
|
|
67
|
-
<TabsList className="grid w-full grid-cols-
|
|
69
|
+
<TabsList className="grid w-full grid-cols-4">
|
|
68
70
|
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
69
71
|
<TabsTrigger value="contacts">
|
|
70
72
|
{record.nb_contacts
|
|
@@ -73,6 +75,13 @@ const CompanyShowContent = () => {
|
|
|
73
75
|
: `${record.nb_contacts} Contacts`
|
|
74
76
|
: "No Contacts"}
|
|
75
77
|
</TabsTrigger>
|
|
78
|
+
<TabsTrigger value="notes">
|
|
79
|
+
{record.nb_notes
|
|
80
|
+
? record.nb_notes === 1
|
|
81
|
+
? "1 Note"
|
|
82
|
+
: `${record.nb_notes} Notes`
|
|
83
|
+
: "Notes"}
|
|
84
|
+
</TabsTrigger>
|
|
76
85
|
{record.nb_deals ? (
|
|
77
86
|
<TabsTrigger value="deals">
|
|
78
87
|
{record.nb_deals === 1
|
|
@@ -111,6 +120,16 @@ const CompanyShowContent = () => {
|
|
|
111
120
|
</div>
|
|
112
121
|
)}
|
|
113
122
|
</TabsContent>
|
|
123
|
+
<TabsContent value="notes">
|
|
124
|
+
<ReferenceManyField
|
|
125
|
+
reference="companyNotes"
|
|
126
|
+
target="company_id"
|
|
127
|
+
sort={{ field: "date", order: "DESC" }}
|
|
128
|
+
empty={<NoteCreate reference="companies" className="mt-4" />}
|
|
129
|
+
>
|
|
130
|
+
<NotesIterator reference="companies" />
|
|
131
|
+
</ReferenceManyField>
|
|
132
|
+
</TabsContent>
|
|
114
133
|
<TabsContent value="deals">
|
|
115
134
|
{record.nb_deals ? (
|
|
116
135
|
<ReferenceManyField
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const COMPANY_CREATED = "company.created" as const;
|
|
2
|
+
export const COMPANY_NOTE_CREATED = "companyNote.created" as const;
|
|
2
3
|
export const CONTACT_CREATED = "contact.created" as const;
|
|
3
4
|
export const CONTACT_NOTE_CREATED = "contactNote.created" as const;
|
|
4
5
|
export const DEAL_CREATED = "deal.created" as const;
|
|
@@ -52,29 +52,45 @@ export const DealsChart = memo(() => {
|
|
|
52
52
|
won: dealsByMonth[month]
|
|
53
53
|
.filter((deal: Deal) => deal.stage === "won")
|
|
54
54
|
.reduce((acc: number, deal: Deal) => {
|
|
55
|
-
|
|
55
|
+
const amount = deal.amount ?? 0;
|
|
56
|
+
if (!isNaN(amount)) {
|
|
57
|
+
acc += amount;
|
|
58
|
+
}
|
|
56
59
|
return acc;
|
|
57
60
|
}, 0),
|
|
58
61
|
pending: dealsByMonth[month]
|
|
59
62
|
.filter((deal: Deal) => !["won", "lost"].includes(deal.stage))
|
|
60
63
|
.reduce((acc: number, deal: Deal) => {
|
|
64
|
+
const amount = deal.amount ?? 0;
|
|
61
65
|
// @ts-expect-error - multiplier type issue
|
|
62
|
-
|
|
66
|
+
const mult = multiplier[deal.stage] ?? 0;
|
|
67
|
+
if (!isNaN(amount) && !isNaN(mult)) {
|
|
68
|
+
acc += amount * mult;
|
|
69
|
+
}
|
|
63
70
|
return acc;
|
|
64
71
|
}, 0),
|
|
65
72
|
lost: dealsByMonth[month]
|
|
66
73
|
.filter((deal: Deal) => deal.stage === "lost")
|
|
67
74
|
.reduce((acc: number, deal: Deal) => {
|
|
68
|
-
|
|
75
|
+
const amount = deal.amount ?? 0;
|
|
76
|
+
if (!isNaN(amount)) {
|
|
77
|
+
acc -= amount;
|
|
78
|
+
}
|
|
69
79
|
return acc;
|
|
70
80
|
}, 0),
|
|
71
81
|
};
|
|
72
82
|
});
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
// Filter out any months with NaN values
|
|
85
|
+
return amountByMonth.filter(
|
|
86
|
+
(month) =>
|
|
87
|
+
!isNaN(month.won) && !isNaN(month.pending) && !isNaN(month.lost),
|
|
88
|
+
);
|
|
75
89
|
}, [data]);
|
|
76
90
|
|
|
77
91
|
if (isPending) return null; // FIXME return skeleton instead
|
|
92
|
+
if (months.length === 0) return null; // No data to display
|
|
93
|
+
|
|
78
94
|
const range = months.reduce(
|
|
79
95
|
(acc, month) => {
|
|
80
96
|
acc.min = Math.min(acc.min, month.lost);
|
|
@@ -83,6 +99,13 @@ export const DealsChart = memo(() => {
|
|
|
83
99
|
},
|
|
84
100
|
{ min: 0, max: 0 },
|
|
85
101
|
);
|
|
102
|
+
|
|
103
|
+
// Ensure we have a valid range for the chart
|
|
104
|
+
const chartRange = {
|
|
105
|
+
min: range.min === 0 && range.max === 0 ? -100 : range.min * 1.2,
|
|
106
|
+
max: range.min === 0 && range.max === 0 ? 100 : range.max * 1.2,
|
|
107
|
+
};
|
|
108
|
+
|
|
86
109
|
return (
|
|
87
110
|
<div className="flex flex-col">
|
|
88
111
|
<div className="flex items-center mb-4">
|
|
@@ -103,8 +126,8 @@ export const DealsChart = memo(() => {
|
|
|
103
126
|
padding={0.3}
|
|
104
127
|
valueScale={{
|
|
105
128
|
type: "linear",
|
|
106
|
-
min:
|
|
107
|
-
max:
|
|
129
|
+
min: chartRange.min,
|
|
130
|
+
max: chartRange.max,
|
|
108
131
|
}}
|
|
109
132
|
indexScale={{ type: "band", round: true }}
|
|
110
133
|
enableGridX={true}
|
|
@@ -20,6 +20,7 @@ import { getCurrentDate } from "./utils";
|
|
|
20
20
|
const foreignKeyMapping = {
|
|
21
21
|
contacts: "contact_id",
|
|
22
22
|
deals: "deal_id",
|
|
23
|
+
companies: "company_id",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export const NoteCreate = ({
|
|
@@ -27,7 +28,7 @@ export const NoteCreate = ({
|
|
|
27
28
|
showStatus,
|
|
28
29
|
className,
|
|
29
30
|
}: {
|
|
30
|
-
reference: "contacts" | "deals";
|
|
31
|
+
reference: "contacts" | "deals" | "companies";
|
|
31
32
|
showStatus?: boolean;
|
|
32
33
|
className?: string;
|
|
33
34
|
}) => {
|
|
@@ -53,7 +54,7 @@ const NoteCreateButton = ({
|
|
|
53
54
|
reference,
|
|
54
55
|
record,
|
|
55
56
|
}: {
|
|
56
|
-
reference: "contacts" | "deals";
|
|
57
|
+
reference: "contacts" | "deals" | "companies";
|
|
57
58
|
record: RaRecord<Identifier>;
|
|
58
59
|
}) => {
|
|
59
60
|
const [update] = useUpdate();
|
|
@@ -82,9 +83,15 @@ const NoteCreateButton = ({
|
|
|
82
83
|
const handleSuccess = (data: any) => {
|
|
83
84
|
reset(resetValues, { keepValues: false });
|
|
84
85
|
refetch();
|
|
86
|
+
|
|
87
|
+
const updateData: any = { last_seen: new Date().toISOString() };
|
|
88
|
+
if (reference === "contacts") {
|
|
89
|
+
updateData.status = data.status;
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
update(reference, {
|
|
86
93
|
id: (record && record.id) as unknown as Identifier,
|
|
87
|
-
data:
|
|
94
|
+
data: updateData,
|
|
88
95
|
previousData: record,
|
|
89
96
|
});
|
|
90
97
|
notify("Note added");
|
|
@@ -2,6 +2,7 @@ import type { DataProvider, Identifier } from "ra-core";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
COMPANY_CREATED,
|
|
5
|
+
COMPANY_NOTE_CREATED,
|
|
5
6
|
CONTACT_CREATED,
|
|
6
7
|
CONTACT_NOTE_CREATED,
|
|
7
8
|
DEAL_CREATED,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
import type {
|
|
11
12
|
Activity,
|
|
12
13
|
Company,
|
|
14
|
+
CompanyNote,
|
|
13
15
|
Contact,
|
|
14
16
|
ContactNote,
|
|
15
17
|
Deal,
|
|
@@ -37,14 +39,15 @@ export async function getActivityLog(
|
|
|
37
39
|
filter["sales_id@in"] = `(${salesId})`;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const [newCompanies, newContactsAndNotes, newDealsAndNotes] =
|
|
42
|
+
const [newCompanies, newContactsAndNotes, newDealsAndNotes, newCompanyNotes] =
|
|
41
43
|
await Promise.all([
|
|
42
44
|
getNewCompanies(dataProvider, companyFilter),
|
|
43
45
|
getNewContactsAndNotes(dataProvider, filter),
|
|
44
46
|
getNewDealsAndNotes(dataProvider, filter),
|
|
47
|
+
getNewCompanyNotes(dataProvider, filter),
|
|
45
48
|
]);
|
|
46
49
|
return (
|
|
47
|
-
[...newCompanies, ...newContactsAndNotes, ...newDealsAndNotes]
|
|
50
|
+
[...newCompanies, ...newContactsAndNotes, ...newDealsAndNotes, ...newCompanyNotes]
|
|
48
51
|
// sort by date desc
|
|
49
52
|
.sort((a, b) =>
|
|
50
53
|
a.date && b.date ? a.date.localeCompare(b.date) * -1 : 0,
|
|
@@ -172,3 +175,36 @@ async function getNewDealsAndNotes(
|
|
|
172
175
|
|
|
173
176
|
return [...newDeals, ...newDealNotes];
|
|
174
177
|
}
|
|
178
|
+
|
|
179
|
+
async function getNewCompanyNotes(
|
|
180
|
+
dataProvider: DataProvider,
|
|
181
|
+
filter: any,
|
|
182
|
+
): Promise<Activity[]> {
|
|
183
|
+
const recentCompanyNotesFilter = {} as any;
|
|
184
|
+
|
|
185
|
+
if (filter.sales_id) {
|
|
186
|
+
recentCompanyNotesFilter.sales_id = filter.sales_id;
|
|
187
|
+
}
|
|
188
|
+
if (filter.company_id) {
|
|
189
|
+
recentCompanyNotesFilter.company_id = filter.company_id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { data: companyNotes } = await dataProvider.getList<CompanyNote>(
|
|
193
|
+
"companyNotes",
|
|
194
|
+
{
|
|
195
|
+
filter: recentCompanyNotesFilter,
|
|
196
|
+
pagination: { page: 1, perPage: 250 },
|
|
197
|
+
sort: { field: "date", order: "DESC" },
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const newCompanyNotes = companyNotes.map((companyNote) => ({
|
|
202
|
+
id: `companyNote.${companyNote.id}.created`,
|
|
203
|
+
type: COMPANY_NOTE_CREATED,
|
|
204
|
+
sales_id: companyNote.sales_id,
|
|
205
|
+
companyNote,
|
|
206
|
+
date: companyNote.date,
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
return newCompanyNotes;
|
|
210
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { datatype, lorem, random } from "faker/locale/en_US";
|
|
2
|
+
|
|
3
|
+
import type { CompanyNote } from "../../../types";
|
|
4
|
+
import type { Db } from "./types";
|
|
5
|
+
import { randomDate } from "./utils";
|
|
6
|
+
|
|
7
|
+
export const generateCompanyNotes = (db: Db): CompanyNote[] => {
|
|
8
|
+
return Array.from(Array(600).keys()).map((id) => {
|
|
9
|
+
const company = random.arrayElement(db.companies);
|
|
10
|
+
const date = randomDate(new Date(company.created_at));
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
company_id: company.id,
|
|
15
|
+
text: lorem.paragraphs(datatype.number({ min: 1, max: 4 })),
|
|
16
|
+
date: date.toISOString(),
|
|
17
|
+
sales_id: company.sales_id,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateCompanies } from "./companies";
|
|
2
|
+
import { generateCompanyNotes } from "./companyNotes";
|
|
2
3
|
import { generateContactNotes } from "./contactNotes";
|
|
3
4
|
import { generateContacts } from "./contacts";
|
|
4
5
|
import { generateDealNotes } from "./dealNotes";
|
|
@@ -14,6 +15,7 @@ export default (): Db => {
|
|
|
14
15
|
db.sales = generateSales(db);
|
|
15
16
|
db.tags = generateTags(db);
|
|
16
17
|
db.companies = generateCompanies(db);
|
|
18
|
+
db.companyNotes = generateCompanyNotes(db);
|
|
17
19
|
db.contacts = generateContacts(db);
|
|
18
20
|
db.contactNotes = generateContactNotes(db);
|
|
19
21
|
db.deals = generateDeals(db);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Company,
|
|
3
|
+
CompanyNote,
|
|
3
4
|
Contact,
|
|
4
5
|
ContactNote,
|
|
5
6
|
Deal,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
|
|
12
13
|
export interface Db {
|
|
13
14
|
companies: Required<Company>[];
|
|
15
|
+
companyNotes: CompanyNote[];
|
|
14
16
|
contacts: Required<Contact>[];
|
|
15
17
|
contactNotes: ContactNote[];
|
|
16
18
|
deals: Deal[];
|
|
@@ -169,6 +169,7 @@ export const CRM = ({
|
|
|
169
169
|
<Resource name="deals" {...deals} />
|
|
170
170
|
<Resource name="contacts" {...contacts} />
|
|
171
171
|
<Resource name="companies" {...companies} />
|
|
172
|
+
<Resource name="companyNotes" />
|
|
172
173
|
<Resource name="contactNotes" />
|
|
173
174
|
<Resource name="dealNotes" />
|
|
174
175
|
<Resource name="tasks" />
|
|
@@ -69,6 +69,8 @@ export type Company = {
|
|
|
69
69
|
context_links?: string[];
|
|
70
70
|
nb_contacts?: number;
|
|
71
71
|
nb_deals?: number;
|
|
72
|
+
nb_notes?: number;
|
|
73
|
+
last_seen?: string;
|
|
72
74
|
} & Pick<RaRecord, "id">;
|
|
73
75
|
|
|
74
76
|
export type EmailAndType = {
|
|
@@ -138,6 +140,17 @@ export type DealNote = {
|
|
|
138
140
|
status?: undefined;
|
|
139
141
|
} & Pick<RaRecord, "id">;
|
|
140
142
|
|
|
143
|
+
export type CompanyNote = {
|
|
144
|
+
company_id: Identifier;
|
|
145
|
+
text: string;
|
|
146
|
+
date: string;
|
|
147
|
+
sales_id: Identifier;
|
|
148
|
+
attachments?: AttachmentNote[];
|
|
149
|
+
|
|
150
|
+
// This is defined for compatibility with `ContactNote`
|
|
151
|
+
status?: undefined;
|
|
152
|
+
} & Pick<RaRecord, "id">;
|
|
153
|
+
|
|
141
154
|
export type Tag = {
|
|
142
155
|
name: string;
|
|
143
156
|
color: string;
|
|
@@ -190,6 +203,13 @@ export type ActivityDealNoteCreated = {
|
|
|
190
203
|
date: string;
|
|
191
204
|
};
|
|
192
205
|
|
|
206
|
+
export type ActivityCompanyNoteCreated = {
|
|
207
|
+
type: typeof COMPANY_NOTE_CREATED;
|
|
208
|
+
sales_id?: Identifier;
|
|
209
|
+
companyNote: CompanyNote;
|
|
210
|
+
date: string;
|
|
211
|
+
};
|
|
212
|
+
|
|
193
213
|
export type Activity = RaRecord &
|
|
194
214
|
(
|
|
195
215
|
| ActivityCompanyCreated
|
|
@@ -197,6 +217,7 @@ export type Activity = RaRecord &
|
|
|
197
217
|
| ActivityContactNoteCreated
|
|
198
218
|
| ActivityDealCreated
|
|
199
219
|
| ActivityDealNoteCreated
|
|
220
|
+
| ActivityCompanyNoteCreated
|
|
200
221
|
);
|
|
201
222
|
|
|
202
223
|
export interface RAFile {
|
|
@@ -2,7 +2,16 @@ import * as React from "react"
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils"
|
|
4
4
|
|
|
5
|
-
function Input({
|
|
5
|
+
function Input({
|
|
6
|
+
className,
|
|
7
|
+
type,
|
|
8
|
+
helperText: _helperText,
|
|
9
|
+
alwaysOn: _alwaysOn,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<"input"> & {
|
|
12
|
+
helperText?: string;
|
|
13
|
+
alwaysOn?: boolean;
|
|
14
|
+
}) {
|
|
6
15
|
return (
|
|
7
16
|
<input
|
|
8
17
|
type={type}
|
|
@@ -2,7 +2,15 @@ import * as React from "react"
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils"
|
|
4
4
|
|
|
5
|
-
function Textarea({
|
|
5
|
+
function Textarea({
|
|
6
|
+
className,
|
|
7
|
+
helperText: _helperText,
|
|
8
|
+
alwaysOn: _alwaysOn,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<"textarea"> & {
|
|
11
|
+
helperText?: string;
|
|
12
|
+
alwaysOn?: boolean;
|
|
13
|
+
}) {
|
|
6
14
|
return (
|
|
7
15
|
<textarea
|
|
8
16
|
data-slot="textarea"
|
|
@@ -31,6 +31,15 @@ DROP INDEX IF EXISTS idx_activities_pending_storage;
|
|
|
31
31
|
-- Remove the CHECK constraint on payload_storage_status to allow NULL and 'in_storage' only
|
|
32
32
|
-- First drop the constraint, then add a new one
|
|
33
33
|
ALTER TABLE activities DROP CONSTRAINT IF EXISTS activities_payload_storage_status_check;
|
|
34
|
+
|
|
35
|
+
-- Clean up any existing data that doesn't match the new constraint
|
|
36
|
+
-- Convert 'pending_move' or any other status to 'in_storage'
|
|
37
|
+
UPDATE activities
|
|
38
|
+
SET payload_storage_status = 'in_storage'
|
|
39
|
+
WHERE payload_storage_status IS NOT NULL
|
|
40
|
+
AND payload_storage_status != 'in_storage';
|
|
41
|
+
|
|
42
|
+
-- Now add the constraint
|
|
34
43
|
ALTER TABLE activities ADD CONSTRAINT activities_payload_storage_status_check
|
|
35
44
|
CHECK (payload_storage_status IS NULL OR payload_storage_status = 'in_storage');
|
|
36
45
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- Create companyNotes table (similar to dealNotes)
|
|
2
|
+
create table "public"."companyNotes" (
|
|
3
|
+
"id" bigint generated by default as identity not null,
|
|
4
|
+
"company_id" bigint not null,
|
|
5
|
+
"text" text,
|
|
6
|
+
"date" timestamp with time zone default now(),
|
|
7
|
+
"sales_id" bigint,
|
|
8
|
+
"attachments" jsonb[]
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- Primary key
|
|
12
|
+
CREATE UNIQUE INDEX "companyNotes_pkey" ON public."companyNotes" USING btree (id);
|
|
13
|
+
alter table "public"."companyNotes" add constraint "companyNotes_pkey" PRIMARY KEY using index "companyNotes_pkey";
|
|
14
|
+
|
|
15
|
+
-- Foreign keys with CASCADE
|
|
16
|
+
alter table "public"."companyNotes" add constraint "companyNotes_company_id_fkey"
|
|
17
|
+
FOREIGN KEY (company_id) REFERENCES companies(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
18
|
+
alter table "public"."companyNotes" validate constraint "companyNotes_company_id_fkey";
|
|
19
|
+
|
|
20
|
+
alter table "public"."companyNotes" add constraint "companyNotes_sales_id_fkey"
|
|
21
|
+
FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
22
|
+
alter table "public"."companyNotes" validate constraint "companyNotes_sales_id_fkey";
|
|
23
|
+
|
|
24
|
+
-- RLS policies (permissive)
|
|
25
|
+
alter table "public"."companyNotes" enable row level security;
|
|
26
|
+
|
|
27
|
+
create policy "Enable insert for authenticated users only"
|
|
28
|
+
on "public"."companyNotes"
|
|
29
|
+
as permissive
|
|
30
|
+
for insert
|
|
31
|
+
to authenticated
|
|
32
|
+
with check (true);
|
|
33
|
+
|
|
34
|
+
create policy "Enable read access for authenticated users"
|
|
35
|
+
on "public"."companyNotes"
|
|
36
|
+
as permissive
|
|
37
|
+
for select
|
|
38
|
+
to authenticated
|
|
39
|
+
using (true);
|
|
40
|
+
|
|
41
|
+
create policy "Company Notes Delete Policy"
|
|
42
|
+
on "public"."companyNotes"
|
|
43
|
+
as permissive
|
|
44
|
+
for delete
|
|
45
|
+
to authenticated
|
|
46
|
+
using (true);
|
|
47
|
+
|
|
48
|
+
create policy "Company Notes Update Policy"
|
|
49
|
+
on "public"."companyNotes"
|
|
50
|
+
as permissive
|
|
51
|
+
for update
|
|
52
|
+
to authenticated
|
|
53
|
+
using (true);
|
|
54
|
+
|
|
55
|
+
-- Grants
|
|
56
|
+
grant delete on table "public"."companyNotes" to "authenticated";
|
|
57
|
+
grant insert on table "public"."companyNotes" to "authenticated";
|
|
58
|
+
grant select on table "public"."companyNotes" to "authenticated";
|
|
59
|
+
grant update on table "public"."companyNotes" to "authenticated";
|
|
60
|
+
|
|
61
|
+
grant delete on table "public"."companyNotes" to "service_role";
|
|
62
|
+
grant insert on table "public"."companyNotes" to "service_role";
|
|
63
|
+
grant references on table "public"."companyNotes" to "service_role";
|
|
64
|
+
grant select on table "public"."companyNotes" to "service_role";
|
|
65
|
+
grant trigger on table "public"."companyNotes" to "service_role";
|
|
66
|
+
grant truncate on table "public"."companyNotes" to "service_role";
|
|
67
|
+
grant update on table "public"."companyNotes" to "service_role";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Drop and recreate view with nb_notes
|
|
2
|
+
drop view if exists "public"."companies_summary";
|
|
3
|
+
|
|
4
|
+
create view "public"."companies_summary"
|
|
5
|
+
with (security_invoker=on)
|
|
6
|
+
as
|
|
7
|
+
select
|
|
8
|
+
c.*,
|
|
9
|
+
count(distinct d.id) as nb_deals,
|
|
10
|
+
count(distinct co.id) as nb_contacts,
|
|
11
|
+
count(distinct cn.id) as nb_notes
|
|
12
|
+
from "public"."companies" c
|
|
13
|
+
left join "public"."deals" d on c.id = d.company_id
|
|
14
|
+
left join "public"."contacts" co on c.id = co.company_id
|
|
15
|
+
left join "public"."companyNotes" cn on c.id = cn.company_id
|
|
16
|
+
group by c.id;
|