create-bw-app 0.10.0 → 0.11.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/README.md +16 -1
- package/bin/bw.mjs +8 -0
- package/package.json +5 -2
- package/src/add.mjs +100 -0
- package/src/adopt.mjs +176 -0
- package/src/app-manifest.mjs +250 -0
- package/src/bw.mjs +53 -0
- package/src/constants.mjs +22 -11
- package/src/diff.mjs +85 -0
- package/src/doctor.mjs +99 -0
- package/src/generator.mjs +68 -12
- package/src/migrations.mjs +92 -0
- package/src/remove.mjs +100 -0
- package/src/scaffold.mjs +82 -0
- package/src/update.mjs +51 -3
- package/src/upgrade.mjs +73 -0
- package/template/base/AGENTS.md +2 -0
- package/template/base/app/globals.css +30 -14
- package/template/base/app/page.tsx +1 -0
- package/template/base/config/bootstrap.ts +1 -1
- package/template/base/config/brand.ts +0 -2
- package/template/base/config/modules.ts +2 -2
- package/template/base/config/shell.overrides.ts +16 -0
- package/template/base/docs/ai/README.md +6 -3
- package/template/base/docs/ai/examples.md +4 -2
- package/template/base/public/brand/logo-dark.svg +2 -2
- package/template/base/public/brand/logo-light.svg +2 -2
- package/template/base/public/brand/logo-mark.svg +2 -2
- package/template/module-manifests/admin/brightweb.module.json +6 -0
- package/template/module-manifests/crm/brightweb.module.json +7 -0
- package/template/module-manifests/orgs/brightweb.module.json +6 -0
- package/template/module-manifests/projects/brightweb.module.json +6 -0
- package/template/modules/crm/app/api/crm/contacts/route.ts +10 -0
- package/template/modules/crm/app/api/crm/timeline/route.ts +8 -0
- package/template/modules/crm/app/crm/layout.tsx +5 -0
- package/template/modules/crm/app/crm/page.tsx +5 -0
- package/template/supabase/module-registry.json +9 -3
- package/template/supabase/modules/crm/README.md +2 -0
- package/template/supabase/modules/crm/migrations/20260316092000_crm_v1.sql +3 -253
- package/template/supabase/modules/crm/migrations/20260316092010_crm_org_integration.sql +66 -0
- package/template/supabase/modules/crm/migrations/20260421201523_portal_read_indexes.sql +19 -0
- package/template/supabase/modules/orgs/README.md +4 -0
- package/template/supabase/modules/orgs/migrations/20260316091500_orgs_v1.sql +216 -0
- package/template/supabase/modules/projects/migrations/20260421201528_portal_read_indexes.sql +6 -0
- package/template/modules/crm/app/playground/crm/page.tsx +0 -103
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
2
|
<rect width="64" height="64" rx="18" fill="#13251A"/>
|
|
3
|
-
<path d="
|
|
4
|
-
<
|
|
3
|
+
<path d="M15 44V20H28.02C32.8 20 35.76 22.42 35.76 26.24C35.76 28.58 34.62 30.28 32.58 31.18C35.22 32.02 36.84 34.04 36.84 36.86C36.84 41.36 33.42 44 28.02 44H15ZM20.36 29.58H27.08C29.08 29.58 30.24 28.62 30.24 26.96C30.24 25.3 29.08 24.36 27.08 24.36H20.36V29.58ZM20.36 39.62H27.64C29.98 39.62 31.28 38.56 31.28 36.68C31.28 34.84 29.98 33.78 27.64 33.78H20.36V39.62Z" fill="#F5F4EC"/>
|
|
4
|
+
<path d="M38.72 20H44.18L46.58 34.74L49.72 20H54.5L49.32 44H44.12L41.18 29.34L38.24 44H33.16L32.26 39.72H37.22L38.72 20Z" fill="#71C48B"/>
|
|
5
5
|
</svg>
|
|
@@ -6,3 +6,13 @@ export const GET = createModuleRouteHandler(
|
|
|
6
6
|
() => import("@brightweblabs/module-crm"),
|
|
7
7
|
"handleCrmContactsGetRequest",
|
|
8
8
|
);
|
|
9
|
+
|
|
10
|
+
export const POST = createModuleRouteHandler(
|
|
11
|
+
() => import("@brightweblabs/module-crm"),
|
|
12
|
+
"handleCrmContactsPostRequest",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const PATCH = createModuleRouteHandler(
|
|
16
|
+
() => import("@brightweblabs/module-crm"),
|
|
17
|
+
"handleCrmContactsPatchRequest",
|
|
18
|
+
);
|
|
@@ -12,16 +12,22 @@
|
|
|
12
12
|
"dependsOn": ["core"],
|
|
13
13
|
"description": "RBAC, user governance, and privileged role-management behavior."
|
|
14
14
|
},
|
|
15
|
+
"orgs": {
|
|
16
|
+
"label": "Organizations",
|
|
17
|
+
"path": "supabase/modules/orgs/migrations",
|
|
18
|
+
"dependsOn": ["core", "admin"],
|
|
19
|
+
"description": "Organizations, membership, invitations, and shared organization access helpers."
|
|
20
|
+
},
|
|
15
21
|
"crm": {
|
|
16
22
|
"label": "CRM",
|
|
17
23
|
"path": "supabase/modules/crm/migrations",
|
|
18
|
-
"dependsOn": ["core", "admin"],
|
|
19
|
-
"description": "
|
|
24
|
+
"dependsOn": ["core", "admin", "orgs"],
|
|
25
|
+
"description": "CRM contacts, status tracking, and organization integration."
|
|
20
26
|
},
|
|
21
27
|
"projects": {
|
|
22
28
|
"label": "Projects",
|
|
23
29
|
"path": "supabase/modules/projects/migrations",
|
|
24
|
-
"dependsOn": ["core", "admin", "
|
|
30
|
+
"dependsOn": ["core", "admin", "orgs"],
|
|
25
31
|
"description": "Projects, tasks, milestones, members, links, and project activity policies."
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
- `organization_invitations`
|
|
12
12
|
- CRM/org helper functions like `is_org_admin()` and `set_crm_status(...)`
|
|
13
13
|
- CRM/org RLS and triggers
|
|
14
|
+
- app-owned operational contact records, not external ESP topic identifiers
|
|
14
15
|
|
|
15
16
|
## Shared boundary
|
|
16
17
|
|
|
17
18
|
Some marketing data is adjacent to CRM, but the Brightweb v1 split is:
|
|
18
19
|
|
|
19
20
|
- CRM owns operational contact and organization management
|
|
21
|
+
- CRM does not currently define newsletter/product-update subscription tables or Resend Topic mappings
|
|
20
22
|
- Marketing automation remains its own future module boundary
|
|
21
23
|
|
|
22
24
|
## Dependency
|
|
@@ -1,80 +1,7 @@
|
|
|
1
1
|
-- Brightweb CRM v1 baseline.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
name text NOT NULL,
|
|
6
|
-
industry text,
|
|
7
|
-
company_size text,
|
|
8
|
-
budget_range text,
|
|
9
|
-
website_url text,
|
|
10
|
-
address text,
|
|
11
|
-
tax_identifier_value text,
|
|
12
|
-
tax_identifier_kind text,
|
|
13
|
-
tax_identifier_country_code text,
|
|
14
|
-
primary_contact_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
15
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
16
|
-
CONSTRAINT organizations_tax_identifier_kind_check
|
|
17
|
-
CHECK (
|
|
18
|
-
tax_identifier_kind IS NULL
|
|
19
|
-
OR tax_identifier_kind IN ('vat', 'tax', 'registration', 'other')
|
|
20
|
-
),
|
|
21
|
-
CONSTRAINT organizations_tax_identifier_country_code_check
|
|
22
|
-
CHECK (
|
|
23
|
-
tax_identifier_country_code IS NULL
|
|
24
|
-
OR tax_identifier_country_code ~ '^[A-Z]{2}$'
|
|
25
|
-
)
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
CREATE INDEX IF NOT EXISTS idx_organizations_primary_contact_id
|
|
29
|
-
ON public.organizations (primary_contact_id);
|
|
30
|
-
|
|
31
|
-
CREATE INDEX IF NOT EXISTS idx_organizations_tax_identifier_lookup
|
|
32
|
-
ON public.organizations (tax_identifier_country_code, tax_identifier_kind, tax_identifier_value)
|
|
33
|
-
WHERE tax_identifier_value IS NOT NULL;
|
|
34
|
-
|
|
35
|
-
CREATE TABLE IF NOT EXISTS public.organization_members (
|
|
36
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
-
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
|
38
|
-
profile_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
39
|
-
role text NOT NULL,
|
|
40
|
-
joined_at timestamptz NOT NULL DEFAULT now(),
|
|
41
|
-
CONSTRAINT organization_members_role_check CHECK (role IN ('admin', 'member')),
|
|
42
|
-
CONSTRAINT organization_members_unique UNIQUE (organization_id, profile_id)
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
CREATE INDEX IF NOT EXISTS idx_organization_members_profile_id
|
|
46
|
-
ON public.organization_members (profile_id);
|
|
47
|
-
|
|
48
|
-
CREATE OR REPLACE FUNCTION public.is_org_admin(target_org_id uuid)
|
|
49
|
-
RETURNS boolean
|
|
50
|
-
LANGUAGE sql
|
|
51
|
-
STABLE
|
|
52
|
-
SECURITY DEFINER
|
|
53
|
-
SET search_path = public
|
|
54
|
-
AS $$
|
|
55
|
-
SELECT EXISTS (
|
|
56
|
-
SELECT 1
|
|
57
|
-
FROM public.organization_members om
|
|
58
|
-
WHERE om.organization_id = target_org_id
|
|
59
|
-
AND om.profile_id = public.current_profile_id()
|
|
60
|
-
AND om.role = 'admin'
|
|
61
|
-
)
|
|
62
|
-
$$;
|
|
63
|
-
|
|
64
|
-
CREATE OR REPLACE FUNCTION public.is_org_member(target_org_id uuid)
|
|
65
|
-
RETURNS boolean
|
|
66
|
-
LANGUAGE sql
|
|
67
|
-
STABLE
|
|
68
|
-
SECURITY DEFINER
|
|
69
|
-
SET search_path = public
|
|
70
|
-
AS $$
|
|
71
|
-
SELECT EXISTS (
|
|
72
|
-
SELECT 1
|
|
73
|
-
FROM public.organization_members om
|
|
74
|
-
WHERE om.organization_id = target_org_id
|
|
75
|
-
AND om.profile_id = public.current_profile_id()
|
|
76
|
-
)
|
|
77
|
-
$$;
|
|
2
|
+
-- Organizations install first from the orgs module. Cross-module invitation
|
|
3
|
+
-- linkage and primary-contact membership synchronization are added by the
|
|
4
|
+
-- following CRM org integration migration.
|
|
78
5
|
|
|
79
6
|
CREATE TABLE IF NOT EXISTS public.crm_contacts (
|
|
80
7
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -187,131 +114,8 @@ BEGIN
|
|
|
187
114
|
END;
|
|
188
115
|
$$;
|
|
189
116
|
|
|
190
|
-
CREATE TABLE IF NOT EXISTS public.organization_invitations (
|
|
191
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
192
|
-
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
|
193
|
-
invited_email text NOT NULL,
|
|
194
|
-
role text NOT NULL CHECK (role IN ('admin', 'member')),
|
|
195
|
-
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')),
|
|
196
|
-
invited_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
197
|
-
accepted_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
198
|
-
accepted_contact_id uuid REFERENCES public.crm_contacts(id) ON DELETE SET NULL,
|
|
199
|
-
accepted_at timestamptz,
|
|
200
|
-
revoked_at timestamptz,
|
|
201
|
-
expires_at timestamptz NOT NULL DEFAULT (now() + interval '14 days'),
|
|
202
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
203
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
CREATE UNIQUE INDEX IF NOT EXISTS organization_invitations_unique_org_email
|
|
207
|
-
ON public.organization_invitations (organization_id, invited_email);
|
|
208
|
-
|
|
209
|
-
CREATE INDEX IF NOT EXISTS idx_organization_invitations_org_status
|
|
210
|
-
ON public.organization_invitations (organization_id, status);
|
|
211
|
-
|
|
212
|
-
CREATE INDEX IF NOT EXISTS idx_organization_invitations_email_status
|
|
213
|
-
ON public.organization_invitations (invited_email, status);
|
|
214
|
-
|
|
215
|
-
CREATE INDEX IF NOT EXISTS idx_organization_invitations_accepted_contact_id
|
|
216
|
-
ON public.organization_invitations (accepted_contact_id)
|
|
217
|
-
WHERE accepted_contact_id IS NOT NULL;
|
|
218
|
-
|
|
219
|
-
CREATE OR REPLACE FUNCTION public.touch_organization_invitations_updated_at()
|
|
220
|
-
RETURNS trigger
|
|
221
|
-
LANGUAGE plpgsql
|
|
222
|
-
SECURITY DEFINER
|
|
223
|
-
SET search_path = public
|
|
224
|
-
AS $$
|
|
225
|
-
BEGIN
|
|
226
|
-
NEW.updated_at := now();
|
|
227
|
-
RETURN NEW;
|
|
228
|
-
END;
|
|
229
|
-
$$;
|
|
230
|
-
|
|
231
|
-
DROP TRIGGER IF EXISTS trg_touch_organization_invitations_updated_at ON public.organization_invitations;
|
|
232
|
-
CREATE TRIGGER trg_touch_organization_invitations_updated_at
|
|
233
|
-
BEFORE UPDATE ON public.organization_invitations
|
|
234
|
-
FOR EACH ROW
|
|
235
|
-
EXECUTE FUNCTION public.touch_organization_invitations_updated_at();
|
|
236
|
-
|
|
237
|
-
CREATE OR REPLACE FUNCTION public.normalize_organization_invitation_email()
|
|
238
|
-
RETURNS trigger
|
|
239
|
-
LANGUAGE plpgsql
|
|
240
|
-
SECURITY DEFINER
|
|
241
|
-
SET search_path = public
|
|
242
|
-
AS $$
|
|
243
|
-
BEGIN
|
|
244
|
-
NEW.invited_email := lower(trim(NEW.invited_email));
|
|
245
|
-
RETURN NEW;
|
|
246
|
-
END;
|
|
247
|
-
$$;
|
|
248
|
-
|
|
249
|
-
DROP TRIGGER IF EXISTS trg_normalize_organization_invitation_email ON public.organization_invitations;
|
|
250
|
-
CREATE TRIGGER trg_normalize_organization_invitation_email
|
|
251
|
-
BEFORE INSERT OR UPDATE OF invited_email ON public.organization_invitations
|
|
252
|
-
FOR EACH ROW
|
|
253
|
-
EXECUTE FUNCTION public.normalize_organization_invitation_email();
|
|
254
|
-
|
|
255
|
-
CREATE OR REPLACE FUNCTION public.sync_org_primary_contact_admin_membership()
|
|
256
|
-
RETURNS trigger
|
|
257
|
-
LANGUAGE plpgsql
|
|
258
|
-
SECURITY DEFINER
|
|
259
|
-
SET search_path = public
|
|
260
|
-
AS $$
|
|
261
|
-
BEGIN
|
|
262
|
-
IF NEW.primary_contact_id IS NOT NULL THEN
|
|
263
|
-
INSERT INTO public.organization_members (organization_id, profile_id, role)
|
|
264
|
-
VALUES (NEW.id, NEW.primary_contact_id, 'admin')
|
|
265
|
-
ON CONFLICT (organization_id, profile_id)
|
|
266
|
-
DO UPDATE SET role = 'admin';
|
|
267
|
-
END IF;
|
|
268
|
-
|
|
269
|
-
RETURN NEW;
|
|
270
|
-
END;
|
|
271
|
-
$$;
|
|
272
|
-
|
|
273
|
-
DROP TRIGGER IF EXISTS trg_sync_org_primary_contact_admin_membership ON public.organizations;
|
|
274
|
-
CREATE TRIGGER trg_sync_org_primary_contact_admin_membership
|
|
275
|
-
AFTER INSERT OR UPDATE OF primary_contact_id ON public.organizations
|
|
276
|
-
FOR EACH ROW
|
|
277
|
-
EXECUTE FUNCTION public.sync_org_primary_contact_admin_membership();
|
|
278
|
-
|
|
279
|
-
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;
|
|
280
|
-
ALTER TABLE public.organization_members ENABLE ROW LEVEL SECURITY;
|
|
281
117
|
ALTER TABLE public.crm_contacts ENABLE ROW LEVEL SECURITY;
|
|
282
118
|
ALTER TABLE public.crm_status_log ENABLE ROW LEVEL SECURITY;
|
|
283
|
-
ALTER TABLE public.organization_invitations ENABLE ROW LEVEL SECURITY;
|
|
284
|
-
|
|
285
|
-
DROP POLICY IF EXISTS "Staff manage organizations" ON public.organizations;
|
|
286
|
-
CREATE POLICY "Staff manage organizations"
|
|
287
|
-
ON public.organizations
|
|
288
|
-
FOR ALL
|
|
289
|
-
TO authenticated
|
|
290
|
-
USING (public.is_staff())
|
|
291
|
-
WITH CHECK (public.is_staff());
|
|
292
|
-
|
|
293
|
-
DROP POLICY IF EXISTS "Staff manage org members" ON public.organization_members;
|
|
294
|
-
CREATE POLICY "Staff manage org members"
|
|
295
|
-
ON public.organization_members
|
|
296
|
-
FOR ALL
|
|
297
|
-
TO authenticated
|
|
298
|
-
USING (public.is_staff())
|
|
299
|
-
WITH CHECK (public.is_staff());
|
|
300
|
-
|
|
301
|
-
DROP POLICY IF EXISTS "Org admins manage org members" ON public.organization_members;
|
|
302
|
-
CREATE POLICY "Org admins manage org members"
|
|
303
|
-
ON public.organization_members
|
|
304
|
-
FOR ALL
|
|
305
|
-
TO authenticated
|
|
306
|
-
USING (public.is_org_admin(organization_id))
|
|
307
|
-
WITH CHECK (public.is_org_admin(organization_id));
|
|
308
|
-
|
|
309
|
-
DROP POLICY IF EXISTS "Members view own org membership" ON public.organization_members;
|
|
310
|
-
CREATE POLICY "Members view own org membership"
|
|
311
|
-
ON public.organization_members
|
|
312
|
-
FOR SELECT
|
|
313
|
-
TO authenticated
|
|
314
|
-
USING (profile_id = public.current_profile_id());
|
|
315
119
|
|
|
316
120
|
DROP POLICY IF EXISTS "Staff manage crm contacts" ON public.crm_contacts;
|
|
317
121
|
CREATE POLICY "Staff manage crm contacts"
|
|
@@ -328,39 +132,6 @@ CREATE POLICY "Staff view crm status log"
|
|
|
328
132
|
TO authenticated
|
|
329
133
|
USING (public.is_staff());
|
|
330
134
|
|
|
331
|
-
DROP POLICY IF EXISTS "Staff manage organization invitations" ON public.organization_invitations;
|
|
332
|
-
CREATE POLICY "Staff manage organization invitations"
|
|
333
|
-
ON public.organization_invitations
|
|
334
|
-
FOR ALL
|
|
335
|
-
TO authenticated
|
|
336
|
-
USING (public.is_staff())
|
|
337
|
-
WITH CHECK (public.is_staff());
|
|
338
|
-
|
|
339
|
-
DROP POLICY IF EXISTS "Org admins manage organization invitations" ON public.organization_invitations;
|
|
340
|
-
CREATE POLICY "Org admins manage organization invitations"
|
|
341
|
-
ON public.organization_invitations
|
|
342
|
-
FOR ALL
|
|
343
|
-
TO authenticated
|
|
344
|
-
USING (public.is_org_admin(organization_id))
|
|
345
|
-
WITH CHECK (public.is_org_admin(organization_id));
|
|
346
|
-
|
|
347
|
-
DROP POLICY IF EXISTS "Invited users view own invitations" ON public.organization_invitations;
|
|
348
|
-
CREATE POLICY "Invited users view own invitations"
|
|
349
|
-
ON public.organization_invitations
|
|
350
|
-
FOR SELECT
|
|
351
|
-
TO authenticated
|
|
352
|
-
USING (
|
|
353
|
-
status = 'pending'
|
|
354
|
-
AND lower(invited_email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
INSERT INTO public.organization_members (organization_id, profile_id, role)
|
|
358
|
-
SELECT o.id, o.primary_contact_id, 'admin'
|
|
359
|
-
FROM public.organizations o
|
|
360
|
-
WHERE o.primary_contact_id IS NOT NULL
|
|
361
|
-
ON CONFLICT (organization_id, profile_id)
|
|
362
|
-
DO UPDATE SET role = 'admin';
|
|
363
|
-
|
|
364
135
|
UPDATE public.crm_contacts c
|
|
365
136
|
SET profile_id = p.id,
|
|
366
137
|
updated_at = now()
|
|
@@ -369,24 +140,3 @@ WHERE c.profile_id IS NULL
|
|
|
369
140
|
AND c.email IS NOT NULL
|
|
370
141
|
AND p.email IS NOT NULL
|
|
371
142
|
AND lower(trim(c.email)) = lower(trim(p.email));
|
|
372
|
-
|
|
373
|
-
UPDATE public.organization_invitations oi
|
|
374
|
-
SET accepted_contact_id = (
|
|
375
|
-
SELECT c.id
|
|
376
|
-
FROM public.crm_contacts c
|
|
377
|
-
WHERE (
|
|
378
|
-
oi.accepted_by_profile_id IS NOT NULL
|
|
379
|
-
AND c.profile_id = oi.accepted_by_profile_id
|
|
380
|
-
)
|
|
381
|
-
OR (
|
|
382
|
-
oi.accepted_by_profile_id IS NULL
|
|
383
|
-
AND oi.invited_email IS NOT NULL
|
|
384
|
-
AND c.email IS NOT NULL
|
|
385
|
-
AND lower(trim(c.email)) = lower(trim(oi.invited_email))
|
|
386
|
-
)
|
|
387
|
-
ORDER BY c.updated_at DESC NULLS LAST, c.created_at DESC NULLS LAST, c.id DESC
|
|
388
|
-
LIMIT 1
|
|
389
|
-
),
|
|
390
|
-
updated_at = now()
|
|
391
|
-
WHERE oi.accepted_contact_id IS NULL
|
|
392
|
-
AND oi.status = 'accepted';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- CRM integration for the independently installable Organizations module.
|
|
2
|
+
-- The orgs baseline keeps accepted_contact_id as a plain uuid and omits this
|
|
3
|
+
-- trigger so it never references CRM-owned objects during an orgs-only install.
|
|
4
|
+
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF NOT EXISTS (
|
|
8
|
+
SELECT 1
|
|
9
|
+
FROM pg_constraint
|
|
10
|
+
WHERE conname = 'organization_invitations_accepted_contact_id_fkey'
|
|
11
|
+
AND conrelid = 'public.organization_invitations'::regclass
|
|
12
|
+
) THEN
|
|
13
|
+
ALTER TABLE public.organization_invitations
|
|
14
|
+
ADD CONSTRAINT organization_invitations_accepted_contact_id_fkey
|
|
15
|
+
FOREIGN KEY (accepted_contact_id)
|
|
16
|
+
REFERENCES public.crm_contacts(id)
|
|
17
|
+
ON DELETE SET NULL;
|
|
18
|
+
END IF;
|
|
19
|
+
END;
|
|
20
|
+
$$;
|
|
21
|
+
|
|
22
|
+
CREATE OR REPLACE FUNCTION public.sync_org_primary_contact_admin_membership()
|
|
23
|
+
RETURNS trigger
|
|
24
|
+
LANGUAGE plpgsql
|
|
25
|
+
SECURITY DEFINER
|
|
26
|
+
SET search_path = public
|
|
27
|
+
AS $$
|
|
28
|
+
BEGIN
|
|
29
|
+
IF NEW.primary_contact_id IS NOT NULL THEN
|
|
30
|
+
INSERT INTO public.organization_members (organization_id, profile_id, role)
|
|
31
|
+
VALUES (NEW.id, NEW.primary_contact_id, 'admin')
|
|
32
|
+
ON CONFLICT (organization_id, profile_id)
|
|
33
|
+
DO UPDATE SET role = 'admin';
|
|
34
|
+
END IF;
|
|
35
|
+
|
|
36
|
+
RETURN NEW;
|
|
37
|
+
END;
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
DROP TRIGGER IF EXISTS trg_sync_org_primary_contact_admin_membership ON public.organizations;
|
|
41
|
+
CREATE TRIGGER trg_sync_org_primary_contact_admin_membership
|
|
42
|
+
AFTER INSERT OR UPDATE OF primary_contact_id ON public.organizations
|
|
43
|
+
FOR EACH ROW
|
|
44
|
+
EXECUTE FUNCTION public.sync_org_primary_contact_admin_membership();
|
|
45
|
+
|
|
46
|
+
UPDATE public.organization_invitations oi
|
|
47
|
+
SET accepted_contact_id = (
|
|
48
|
+
SELECT c.id
|
|
49
|
+
FROM public.crm_contacts c
|
|
50
|
+
WHERE (
|
|
51
|
+
oi.accepted_by_profile_id IS NOT NULL
|
|
52
|
+
AND c.profile_id = oi.accepted_by_profile_id
|
|
53
|
+
)
|
|
54
|
+
OR (
|
|
55
|
+
oi.accepted_by_profile_id IS NULL
|
|
56
|
+
AND oi.invited_email IS NOT NULL
|
|
57
|
+
AND c.email IS NOT NULL
|
|
58
|
+
AND lower(trim(c.email)) = lower(trim(oi.invited_email))
|
|
59
|
+
)
|
|
60
|
+
ORDER BY c.updated_at DESC NULLS LAST, c.created_at DESC NULLS LAST, c.id DESC
|
|
61
|
+
LIMIT 1
|
|
62
|
+
),
|
|
63
|
+
updated_at = now()
|
|
64
|
+
WHERE oi.accepted_contact_id IS NULL
|
|
65
|
+
AND oi.status = 'accepted';
|
|
66
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- portal_read_indexes
|
|
2
|
+
-- target: crm
|
|
3
|
+
-- created_at: 2026-04-21T20:15:23.686Z
|
|
4
|
+
|
|
5
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_updated_at_desc
|
|
6
|
+
ON public.crm_contacts (updated_at DESC);
|
|
7
|
+
|
|
8
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_created_at_desc
|
|
9
|
+
ON public.crm_contacts (created_at DESC);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_status
|
|
12
|
+
ON public.crm_contacts (status);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_source
|
|
15
|
+
ON public.crm_contacts (source)
|
|
16
|
+
WHERE source IS NOT NULL;
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_crm_status_log_changed_at_desc
|
|
19
|
+
ON public.crm_status_log (changed_at DESC);
|