create-bw-app 0.9.5 → 0.9.6
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 +2 -0
- package/package.json +1 -1
- package/src/generator.mjs +100 -36
- package/template/supabase/README.md +74 -0
- package/template/supabase/clients/README.md +19 -0
- package/template/supabase/module-registry.json +28 -0
- package/template/supabase/modules/admin/README.md +18 -0
- package/template/supabase/modules/admin/migrations/.gitkeep +1 -0
- package/template/supabase/modules/admin/migrations/20260316091000_admin_v1.sql +317 -0
- package/template/supabase/modules/core/README.md +24 -0
- package/template/supabase/modules/core/migrations/.gitkeep +1 -0
- package/template/supabase/modules/core/migrations/20260316090000_core_v1.sql +497 -0
- package/template/supabase/modules/crm/README.md +27 -0
- package/template/supabase/modules/crm/migrations/.gitkeep +1 -0
- package/template/supabase/modules/crm/migrations/20260316092000_crm_v1.sql +392 -0
- package/template/supabase/modules/projects/README.md +22 -0
- package/template/supabase/modules/projects/migrations/.gitkeep +1 -0
- package/template/supabase/modules/projects/migrations/20260316093000_projects_v1.sql +1120 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
-- Brightweb CRM v1 baseline.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS public.organizations (
|
|
4
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
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
|
+
$$;
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS public.crm_contacts (
|
|
80
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
81
|
+
organization_id uuid REFERENCES public.organizations(id) ON DELETE SET NULL,
|
|
82
|
+
profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
83
|
+
owner_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
84
|
+
first_name text,
|
|
85
|
+
last_name text,
|
|
86
|
+
email text,
|
|
87
|
+
phone text,
|
|
88
|
+
status text NOT NULL DEFAULT 'lead',
|
|
89
|
+
source text,
|
|
90
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
91
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
92
|
+
CONSTRAINT crm_contacts_status_check CHECK (status IN ('lead', 'qualified', 'proposal', 'won', 'lost'))
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_organization_id
|
|
96
|
+
ON public.crm_contacts (organization_id);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_owner_id
|
|
99
|
+
ON public.crm_contacts (owner_id);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_crm_contacts_profile_id
|
|
102
|
+
ON public.crm_contacts (profile_id)
|
|
103
|
+
WHERE profile_id IS NOT NULL;
|
|
104
|
+
|
|
105
|
+
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_email_unique
|
|
106
|
+
ON public.crm_contacts (email)
|
|
107
|
+
WHERE email IS NOT NULL;
|
|
108
|
+
|
|
109
|
+
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_profile_id_unique
|
|
110
|
+
ON public.crm_contacts (profile_id)
|
|
111
|
+
WHERE profile_id IS NOT NULL;
|
|
112
|
+
|
|
113
|
+
CREATE OR REPLACE FUNCTION public.set_crm_contacts_updated_at()
|
|
114
|
+
RETURNS trigger
|
|
115
|
+
LANGUAGE plpgsql
|
|
116
|
+
SECURITY DEFINER
|
|
117
|
+
SET search_path = public
|
|
118
|
+
AS $$
|
|
119
|
+
BEGIN
|
|
120
|
+
NEW.updated_at := now();
|
|
121
|
+
RETURN NEW;
|
|
122
|
+
END;
|
|
123
|
+
$$;
|
|
124
|
+
|
|
125
|
+
DROP TRIGGER IF EXISTS set_crm_contacts_updated_at ON public.crm_contacts;
|
|
126
|
+
CREATE TRIGGER set_crm_contacts_updated_at
|
|
127
|
+
BEFORE UPDATE ON public.crm_contacts
|
|
128
|
+
FOR EACH ROW
|
|
129
|
+
EXECUTE FUNCTION public.set_crm_contacts_updated_at();
|
|
130
|
+
|
|
131
|
+
CREATE TABLE IF NOT EXISTS public.crm_status_log (
|
|
132
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
133
|
+
contact_id uuid NOT NULL REFERENCES public.crm_contacts(id) ON DELETE CASCADE,
|
|
134
|
+
previous_status text,
|
|
135
|
+
new_status text NOT NULL,
|
|
136
|
+
reason text,
|
|
137
|
+
changed_by_user_id uuid,
|
|
138
|
+
changed_at timestamptz NOT NULL DEFAULT now()
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_crm_status_log_profile_id
|
|
142
|
+
ON public.crm_status_log (contact_id);
|
|
143
|
+
|
|
144
|
+
CREATE OR REPLACE FUNCTION public.set_crm_status(
|
|
145
|
+
p_contact_id uuid,
|
|
146
|
+
p_new_status text,
|
|
147
|
+
p_reason text DEFAULT NULL
|
|
148
|
+
)
|
|
149
|
+
RETURNS void
|
|
150
|
+
LANGUAGE plpgsql
|
|
151
|
+
SECURITY DEFINER
|
|
152
|
+
SET search_path = public
|
|
153
|
+
AS $$
|
|
154
|
+
DECLARE
|
|
155
|
+
v_current text;
|
|
156
|
+
BEGIN
|
|
157
|
+
SELECT status INTO v_current
|
|
158
|
+
FROM public.crm_contacts
|
|
159
|
+
WHERE id = p_contact_id;
|
|
160
|
+
|
|
161
|
+
IF v_current IS NULL THEN
|
|
162
|
+
v_current := 'none';
|
|
163
|
+
END IF;
|
|
164
|
+
|
|
165
|
+
IF v_current = p_new_status THEN
|
|
166
|
+
RETURN;
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
UPDATE public.crm_contacts
|
|
170
|
+
SET status = p_new_status,
|
|
171
|
+
updated_at = now()
|
|
172
|
+
WHERE id = p_contact_id;
|
|
173
|
+
|
|
174
|
+
INSERT INTO public.crm_status_log (
|
|
175
|
+
contact_id,
|
|
176
|
+
previous_status,
|
|
177
|
+
new_status,
|
|
178
|
+
reason,
|
|
179
|
+
changed_by_user_id
|
|
180
|
+
) VALUES (
|
|
181
|
+
p_contact_id,
|
|
182
|
+
v_current,
|
|
183
|
+
p_new_status,
|
|
184
|
+
p_reason,
|
|
185
|
+
auth.uid()
|
|
186
|
+
);
|
|
187
|
+
END;
|
|
188
|
+
$$;
|
|
189
|
+
|
|
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
|
+
ALTER TABLE public.crm_contacts ENABLE ROW LEVEL SECURITY;
|
|
282
|
+
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
|
+
|
|
316
|
+
DROP POLICY IF EXISTS "Staff manage crm contacts" ON public.crm_contacts;
|
|
317
|
+
CREATE POLICY "Staff manage crm contacts"
|
|
318
|
+
ON public.crm_contacts
|
|
319
|
+
FOR ALL
|
|
320
|
+
TO authenticated
|
|
321
|
+
USING (public.is_staff())
|
|
322
|
+
WITH CHECK (public.is_staff());
|
|
323
|
+
|
|
324
|
+
DROP POLICY IF EXISTS "Staff view crm status log" ON public.crm_status_log;
|
|
325
|
+
CREATE POLICY "Staff view crm status log"
|
|
326
|
+
ON public.crm_status_log
|
|
327
|
+
FOR SELECT
|
|
328
|
+
TO authenticated
|
|
329
|
+
USING (public.is_staff());
|
|
330
|
+
|
|
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
|
+
UPDATE public.crm_contacts c
|
|
365
|
+
SET profile_id = p.id,
|
|
366
|
+
updated_at = now()
|
|
367
|
+
FROM public.profiles p
|
|
368
|
+
WHERE c.profile_id IS NULL
|
|
369
|
+
AND c.email IS NOT NULL
|
|
370
|
+
AND p.email IS NOT NULL
|
|
371
|
+
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,22 @@
|
|
|
1
|
+
# Projects Migrations
|
|
2
|
+
|
|
3
|
+
`projects` owns work-management and collaboration tables.
|
|
4
|
+
|
|
5
|
+
## Owns
|
|
6
|
+
|
|
7
|
+
- `projects`
|
|
8
|
+
- `project_members`
|
|
9
|
+
- `project_milestones`
|
|
10
|
+
- `project_tasks`
|
|
11
|
+
- `project_links`
|
|
12
|
+
- `project_status_log`
|
|
13
|
+
- project access helpers
|
|
14
|
+
- project activity triggers and policies
|
|
15
|
+
|
|
16
|
+
## Dependency
|
|
17
|
+
|
|
18
|
+
Depends on:
|
|
19
|
+
|
|
20
|
+
- `core`
|
|
21
|
+
- `admin` for global role helpers
|
|
22
|
+
- `crm` when project access depends on organizations or organization membership
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|