realtimex-crm 0.11.0 → 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.
Files changed (29) hide show
  1. package/dist/assets/{DealList-B6Pgm8oq.js → DealList-BvoXZ_PI.js} +3 -3
  2. package/dist/assets/{DealList-B6Pgm8oq.js.map → DealList-BvoXZ_PI.js.map} +1 -1
  3. package/dist/assets/{index-BsUdjhpC.js → index-D-i5w9_j.js} +54 -54
  4. package/dist/assets/{index-BsUdjhpC.js.map → index-D-i5w9_j.js.map} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/stats.html +1 -1
  7. package/package.json +1 -1
  8. package/src/components/atomic-crm/activity/ActivityLogCompanyNoteCreated.tsx +60 -0
  9. package/src/components/atomic-crm/activity/ActivityLogIterator.tsx +6 -0
  10. package/src/components/atomic-crm/companies/CompanyListFilter.tsx +2 -0
  11. package/src/components/atomic-crm/companies/CompanyShow.tsx +20 -1
  12. package/src/components/atomic-crm/consts.ts +1 -0
  13. package/src/components/atomic-crm/dashboard/DealsChart.tsx +29 -6
  14. package/src/components/atomic-crm/deals/stages.test.ts +54 -0
  15. package/src/components/atomic-crm/deals/stages.ts +3 -1
  16. package/src/components/atomic-crm/notes/NoteCreate.tsx +10 -3
  17. package/src/components/atomic-crm/notes/NotesIterator.tsx +1 -1
  18. package/src/components/atomic-crm/providers/commons/activity.ts +38 -2
  19. package/src/components/atomic-crm/providers/fakerest/dataGenerator/companyNotes.ts +20 -0
  20. package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +2 -0
  21. package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +2 -0
  22. package/src/components/atomic-crm/root/CRM.tsx +1 -0
  23. package/src/components/atomic-crm/types.ts +21 -0
  24. package/src/components/ui/input.tsx +10 -1
  25. package/src/components/ui/textarea.tsx +9 -1
  26. package/supabase/migrations/20251223185638_remove_large_payload_storage.sql +9 -0
  27. package/supabase/migrations/20251224000002_add_company_notes.sql +67 -0
  28. package/supabase/migrations/20251224000003_add_company_last_seen.sql +5 -0
  29. 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.11.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
+ &nbsp;added a note about&nbsp;
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-3">
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
- acc += deal.amount;
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
- acc += deal.amount * multiplier[deal.stage];
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
- acc -= deal.amount;
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
- return amountByMonth;
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: range.min * 1.2,
107
- max: range.max * 1.2,
129
+ min: chartRange.min,
130
+ max: chartRange.max,
108
131
  }}
109
132
  indexScale={{ type: "band", round: true }}
110
133
  enableGridX={true}
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getDealsByStage } from './stages';
3
+ import type { Deal } from '../types';
4
+
5
+ describe('getDealsByStage', () => {
6
+ const mockDealStages = [
7
+ { value: 'stage1', label: 'Stage 1' },
8
+ { value: 'stage2', label: 'Stage 2' },
9
+ ];
10
+
11
+ it('should group deals by stage', () => {
12
+ const deals = [
13
+ { id: 1, stage: 'stage1', index: 0, name: 'Deal 1' },
14
+ { id: 2, stage: 'stage2', index: 0, name: 'Deal 2' },
15
+ { id: 3, stage: 'stage1', index: 1, name: 'Deal 3' },
16
+ ] as unknown as Deal[];
17
+
18
+ const result = getDealsByStage(deals, mockDealStages);
19
+
20
+ expect(result['stage1']).toHaveLength(2);
21
+ expect(result['stage2']).toHaveLength(1);
22
+ expect(result['stage1'].map(d => d.name)).toEqual(['Deal 1', 'Deal 3']);
23
+ });
24
+
25
+ it('should ignore deals with unknown stages instead of crashing', () => {
26
+ const deals = [
27
+ { id: 1, stage: 'stage1', index: 0, name: 'Deal 1' },
28
+ { id: 4, stage: 'unknown_stage', index: 0, name: 'Deal 4' }, // Malformed data
29
+ ] as unknown as Deal[];
30
+
31
+ const result = getDealsByStage(deals, mockDealStages);
32
+
33
+ expect(result['stage1']).toHaveLength(1);
34
+ expect(result).not.toHaveProperty('unknown_stage');
35
+ });
36
+
37
+ it('should handle empty deal list', () => {
38
+ const deals: Deal[] = [];
39
+ const result = getDealsByStage(deals, mockDealStages);
40
+ expect(result['stage1']).toEqual([]);
41
+ expect(result['stage2']).toEqual([]);
42
+ });
43
+
44
+ it('should sort deals by index', () => {
45
+ const deals = [
46
+ { id: 1, stage: 'stage1', index: 2, name: 'Deal 1' },
47
+ { id: 3, stage: 'stage1', index: 1, name: 'Deal 3' },
48
+ ] as unknown as Deal[];
49
+
50
+ const result = getDealsByStage(deals, mockDealStages);
51
+ expect(result['stage1'][0].index).toBe(1);
52
+ expect(result['stage1'][1].index).toBe(2);
53
+ });
54
+ });
@@ -10,7 +10,9 @@ export const getDealsByStage = (
10
10
  if (!dealStages) return {};
11
11
  const dealsByStage: Record<Deal["stage"], Deal[]> = unorderedDeals.reduce(
12
12
  (acc, deal) => {
13
- acc[deal.stage].push(deal);
13
+ if (acc[deal.stage]) {
14
+ acc[deal.stage].push(deal);
15
+ }
14
16
  return acc;
15
17
  },
16
18
  dealStages.reduce(
@@ -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: { last_seen: new Date().toISOString(), status: data.status },
94
+ data: updateData,
88
95
  previousData: record,
89
96
  });
90
97
  notify("Note added");
@@ -9,7 +9,7 @@ export const NotesIterator = ({
9
9
  reference,
10
10
  showStatus,
11
11
  }: {
12
- reference: "contacts" | "deals";
12
+ reference: "contacts" | "deals" | "companies";
13
13
  showStatus?: boolean;
14
14
  }) => {
15
15
  const { data, error, isPending } = useListContext();
@@ -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({ className, type, ...props }: React.ComponentProps<"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({ className, ...props }: React.ComponentProps<"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,5 @@
1
+ -- Add last_seen to companies for activity tracking
2
+ alter table "public"."companies" add column "last_seen" timestamp with time zone;
3
+
4
+ -- Backfill with created_at
5
+ update "public"."companies" set last_seen = created_at where last_seen is null;