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.
@@ -0,0 +1,1120 @@
1
+ -- Brightweb projects v1 baseline.
2
+
3
+ CREATE TABLE IF NOT EXISTS public.projects (
4
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
6
+ name text NOT NULL,
7
+ code text,
8
+ status text NOT NULL DEFAULT 'planned',
9
+ health text NOT NULL DEFAULT 'on_track',
10
+ owner_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
11
+ activated_at timestamptz,
12
+ target_date date,
13
+ completed_at timestamptz,
14
+ cancellation_reason text,
15
+ summary text,
16
+ enabled_modules text[] NOT NULL DEFAULT ARRAY[
17
+ 'project-core',
18
+ 'task-execution',
19
+ 'milestones',
20
+ 'client-portal-readonly',
21
+ 'project-links',
22
+ 'activity-feed'
23
+ ]::text[],
24
+ created_at timestamptz NOT NULL DEFAULT now(),
25
+ updated_at timestamptz NOT NULL DEFAULT now(),
26
+ CONSTRAINT projects_status_check CHECK (status IN ('planned', 'active', 'blocked', 'completed', 'canceled')),
27
+ CONSTRAINT projects_health_check CHECK (health IN ('on_track', 'at_risk', 'off_track')),
28
+ CONSTRAINT projects_name_check CHECK (length(btrim(name)) > 0),
29
+ CONSTRAINT projects_code_check CHECK (code IS NULL OR length(btrim(code)) > 0)
30
+ );
31
+
32
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_code_unique
33
+ ON public.projects ((lower(code)))
34
+ WHERE code IS NOT NULL;
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_projects_org
37
+ ON public.projects (organization_id);
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_projects_owner
40
+ ON public.projects (owner_profile_id);
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_projects_status
43
+ ON public.projects (status);
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_projects_health
46
+ ON public.projects (health);
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_projects_target_date
49
+ ON public.projects (target_date);
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_projects_updated_at_desc
52
+ ON public.projects (updated_at DESC);
53
+
54
+ CREATE TABLE IF NOT EXISTS public.project_members (
55
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
56
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
57
+ profile_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
58
+ role text NOT NULL DEFAULT 'contributor',
59
+ created_at timestamptz NOT NULL DEFAULT now(),
60
+ CONSTRAINT project_members_role_check CHECK (role IN ('owner', 'contributor', 'observer')),
61
+ CONSTRAINT project_members_unique UNIQUE (project_id, profile_id)
62
+ );
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_project_members_project
65
+ ON public.project_members (project_id);
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_project_members_profile
68
+ ON public.project_members (profile_id);
69
+
70
+ CREATE TABLE IF NOT EXISTS public.project_milestones (
71
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
72
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
73
+ title text NOT NULL,
74
+ status text NOT NULL DEFAULT 'pending',
75
+ target_date date,
76
+ completed_at timestamptz,
77
+ position integer NOT NULL DEFAULT 0,
78
+ created_at timestamptz NOT NULL DEFAULT now(),
79
+ updated_at timestamptz NOT NULL DEFAULT now(),
80
+ CONSTRAINT project_milestones_status_check CHECK (status IN ('pending', 'in_progress', 'achieved', 'delayed')),
81
+ CONSTRAINT project_milestones_title_check CHECK (length(btrim(title)) > 0)
82
+ );
83
+
84
+ CREATE INDEX IF NOT EXISTS idx_project_milestones_project
85
+ ON public.project_milestones (project_id);
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_project_milestones_position
88
+ ON public.project_milestones (project_id, position);
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_project_milestones_target_date
91
+ ON public.project_milestones (target_date);
92
+
93
+ CREATE TABLE IF NOT EXISTS public.project_tasks (
94
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
95
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
96
+ milestone_id uuid REFERENCES public.project_milestones(id) ON DELETE SET NULL,
97
+ title text NOT NULL,
98
+ description text,
99
+ status text NOT NULL DEFAULT 'todo',
100
+ priority text NOT NULL DEFAULT 'medium',
101
+ assignee_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
102
+ reporter_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
103
+ due_date date,
104
+ position integer NOT NULL DEFAULT 0,
105
+ blocked_reason text,
106
+ created_at timestamptz NOT NULL DEFAULT now(),
107
+ updated_at timestamptz NOT NULL DEFAULT now(),
108
+ CONSTRAINT project_tasks_status_check CHECK (status IN ('todo', 'in_progress', 'blocked', 'done')),
109
+ CONSTRAINT project_tasks_priority_check CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
110
+ CONSTRAINT project_tasks_title_check CHECK (length(btrim(title)) > 0)
111
+ );
112
+
113
+ CREATE INDEX IF NOT EXISTS idx_project_tasks_project
114
+ ON public.project_tasks (project_id);
115
+
116
+ CREATE INDEX IF NOT EXISTS idx_project_tasks_project_status
117
+ ON public.project_tasks (project_id, status);
118
+
119
+ CREATE INDEX IF NOT EXISTS idx_project_tasks_project_position
120
+ ON public.project_tasks (project_id, position);
121
+
122
+ CREATE INDEX IF NOT EXISTS idx_project_tasks_due_date
123
+ ON public.project_tasks (due_date);
124
+
125
+ CREATE INDEX IF NOT EXISTS idx_project_tasks_assignee
126
+ ON public.project_tasks (assignee_profile_id);
127
+
128
+ CREATE TABLE IF NOT EXISTS public.project_links (
129
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
130
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
131
+ label text NOT NULL,
132
+ url text NOT NULL,
133
+ visibility text NOT NULL DEFAULT 'staff',
134
+ kind text NOT NULL DEFAULT 'other',
135
+ created_at timestamptz NOT NULL DEFAULT now(),
136
+ updated_at timestamptz NOT NULL DEFAULT now(),
137
+ CONSTRAINT project_links_visibility_check CHECK (visibility IN ('staff', 'client')),
138
+ CONSTRAINT project_links_kind_check CHECK (kind IN ('doc', 'sheet', 'drive', 'other')),
139
+ CONSTRAINT project_links_label_check CHECK (length(btrim(label)) > 0),
140
+ CONSTRAINT project_links_url_check CHECK (url ~* '^https?://')
141
+ );
142
+
143
+ CREATE INDEX IF NOT EXISTS idx_project_links_project
144
+ ON public.project_links (project_id);
145
+
146
+ CREATE INDEX IF NOT EXISTS idx_project_links_visibility
147
+ ON public.project_links (visibility);
148
+
149
+ CREATE TABLE IF NOT EXISTS public.project_status_log (
150
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
151
+ project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
152
+ previous_status text,
153
+ new_status text NOT NULL,
154
+ reason text,
155
+ changed_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
156
+ changed_at timestamptz NOT NULL DEFAULT now(),
157
+ CONSTRAINT project_status_log_new_status_check CHECK (new_status IN ('planned', 'active', 'blocked', 'completed', 'canceled'))
158
+ );
159
+
160
+ CREATE INDEX IF NOT EXISTS idx_project_status_log_project_changed_at_desc
161
+ ON public.project_status_log (project_id, changed_at DESC);
162
+
163
+ CREATE OR REPLACE FUNCTION public.set_project_updated_at()
164
+ RETURNS trigger
165
+ LANGUAGE plpgsql
166
+ SECURITY DEFINER
167
+ SET search_path = public
168
+ AS $$
169
+ BEGIN
170
+ NEW.updated_at := now();
171
+ RETURN NEW;
172
+ END;
173
+ $$;
174
+
175
+ DROP TRIGGER IF EXISTS trg_projects_updated_at ON public.projects;
176
+ CREATE TRIGGER trg_projects_updated_at
177
+ BEFORE UPDATE ON public.projects
178
+ FOR EACH ROW
179
+ EXECUTE FUNCTION public.set_project_updated_at();
180
+
181
+ DROP TRIGGER IF EXISTS trg_project_milestones_updated_at ON public.project_milestones;
182
+ CREATE TRIGGER trg_project_milestones_updated_at
183
+ BEFORE UPDATE ON public.project_milestones
184
+ FOR EACH ROW
185
+ EXECUTE FUNCTION public.set_project_updated_at();
186
+
187
+ DROP TRIGGER IF EXISTS trg_project_tasks_updated_at ON public.project_tasks;
188
+ CREATE TRIGGER trg_project_tasks_updated_at
189
+ BEFORE UPDATE ON public.project_tasks
190
+ FOR EACH ROW
191
+ EXECUTE FUNCTION public.set_project_updated_at();
192
+
193
+ DROP TRIGGER IF EXISTS trg_project_links_updated_at ON public.project_links;
194
+ CREATE TRIGGER trg_project_links_updated_at
195
+ BEFORE UPDATE ON public.project_links
196
+ FOR EACH ROW
197
+ EXECUTE FUNCTION public.set_project_updated_at();
198
+
199
+ CREATE OR REPLACE FUNCTION public.sync_project_lifecycle_dates()
200
+ RETURNS trigger
201
+ LANGUAGE plpgsql
202
+ SECURITY DEFINER
203
+ SET search_path = public
204
+ AS $$
205
+ BEGIN
206
+ IF TG_OP = 'INSERT' THEN
207
+ IF NEW.status IN ('active', 'completed') AND NEW.activated_at IS NULL THEN
208
+ NEW.activated_at := now();
209
+ END IF;
210
+
211
+ IF NEW.status = 'completed' AND NEW.completed_at IS NULL THEN
212
+ NEW.completed_at := now();
213
+ END IF;
214
+
215
+ RETURN NEW;
216
+ END IF;
217
+
218
+ IF NEW.status = 'active'
219
+ AND OLD.status IS DISTINCT FROM 'active'
220
+ AND NEW.activated_at IS NULL THEN
221
+ NEW.activated_at := now();
222
+ END IF;
223
+
224
+ IF NEW.status = 'completed'
225
+ AND OLD.status IS DISTINCT FROM 'completed'
226
+ AND NEW.completed_at IS NULL THEN
227
+ NEW.completed_at := now();
228
+ END IF;
229
+
230
+ RETURN NEW;
231
+ END;
232
+ $$;
233
+
234
+ DROP TRIGGER IF EXISTS trg_projects_lifecycle_dates ON public.projects;
235
+ CREATE TRIGGER trg_projects_lifecycle_dates
236
+ BEFORE INSERT OR UPDATE ON public.projects
237
+ FOR EACH ROW
238
+ EXECUTE FUNCTION public.sync_project_lifecycle_dates();
239
+
240
+ CREATE OR REPLACE FUNCTION public.is_project_org_admin(target_project_id uuid)
241
+ RETURNS boolean
242
+ LANGUAGE sql
243
+ STABLE
244
+ SECURITY DEFINER
245
+ SET search_path = public
246
+ AS $$
247
+ SELECT EXISTS (
248
+ SELECT 1
249
+ FROM public.projects p
250
+ WHERE p.id = target_project_id
251
+ AND public.is_org_admin(p.organization_id)
252
+ )
253
+ $$;
254
+
255
+ CREATE OR REPLACE FUNCTION public.is_project_org_member(target_project_id uuid)
256
+ RETURNS boolean
257
+ LANGUAGE sql
258
+ STABLE
259
+ SECURITY DEFINER
260
+ SET search_path = public
261
+ AS $$
262
+ SELECT EXISTS (
263
+ SELECT 1
264
+ FROM public.projects p
265
+ WHERE p.id = target_project_id
266
+ AND public.is_org_member(p.organization_id)
267
+ )
268
+ $$;
269
+
270
+ CREATE OR REPLACE FUNCTION public.can_create_projects_for_org(target_org_id uuid)
271
+ RETURNS boolean
272
+ LANGUAGE sql
273
+ STABLE
274
+ SECURITY DEFINER
275
+ SET search_path = public
276
+ AS $$
277
+ SELECT public.is_org_admin(target_org_id)
278
+ $$;
279
+
280
+ CREATE OR REPLACE FUNCTION public.is_project_team_member(target_project_id uuid)
281
+ RETURNS boolean
282
+ LANGUAGE sql
283
+ STABLE
284
+ SECURITY DEFINER
285
+ SET search_path = public
286
+ AS $$
287
+ SELECT EXISTS (
288
+ SELECT 1
289
+ FROM public.projects p
290
+ WHERE p.id = target_project_id
291
+ AND (
292
+ p.owner_profile_id = public.current_profile_id()
293
+ OR EXISTS (
294
+ SELECT 1
295
+ FROM public.project_members pm
296
+ WHERE pm.project_id = p.id
297
+ AND pm.profile_id = public.current_profile_id()
298
+ )
299
+ )
300
+ )
301
+ $$;
302
+
303
+ CREATE OR REPLACE FUNCTION public.is_project_content_editor(target_project_id uuid)
304
+ RETURNS boolean
305
+ LANGUAGE sql
306
+ STABLE
307
+ SECURITY DEFINER
308
+ SET search_path = public
309
+ AS $$
310
+ SELECT EXISTS (
311
+ SELECT 1
312
+ FROM public.projects p
313
+ WHERE p.id = target_project_id
314
+ AND (
315
+ p.owner_profile_id = public.current_profile_id()
316
+ OR EXISTS (
317
+ SELECT 1
318
+ FROM public.project_members pm
319
+ WHERE pm.project_id = p.id
320
+ AND pm.profile_id = public.current_profile_id()
321
+ AND pm.role IN ('owner', 'contributor')
322
+ )
323
+ )
324
+ )
325
+ $$;
326
+
327
+ CREATE OR REPLACE FUNCTION public.is_project_owner_member(target_project_id uuid)
328
+ RETURNS boolean
329
+ LANGUAGE sql
330
+ STABLE
331
+ SECURITY DEFINER
332
+ SET search_path = public
333
+ AS $$
334
+ SELECT EXISTS (
335
+ SELECT 1
336
+ FROM public.projects p
337
+ WHERE p.id = target_project_id
338
+ AND (
339
+ p.owner_profile_id = public.current_profile_id()
340
+ OR EXISTS (
341
+ SELECT 1
342
+ FROM public.project_members pm
343
+ WHERE pm.project_id = p.id
344
+ AND pm.profile_id = public.current_profile_id()
345
+ AND pm.role = 'owner'
346
+ )
347
+ )
348
+ )
349
+ $$;
350
+
351
+ CREATE OR REPLACE FUNCTION public.project_activity_project_name(
352
+ p_project_id uuid
353
+ )
354
+ RETURNS text
355
+ LANGUAGE sql
356
+ STABLE
357
+ SECURITY DEFINER
358
+ SET search_path = public
359
+ AS $$
360
+ SELECT p.name
361
+ FROM public.projects p
362
+ WHERE p.id = p_project_id;
363
+ $$;
364
+
365
+ CREATE OR REPLACE FUNCTION public.log_project_activity_event(
366
+ p_event_type text,
367
+ p_entity_table text,
368
+ p_entity_id uuid,
369
+ p_project_id uuid,
370
+ p_summary text,
371
+ p_payload jsonb DEFAULT '{}'::jsonb,
372
+ p_actor_profile_id uuid DEFAULT NULL
373
+ )
374
+ RETURNS void
375
+ LANGUAGE plpgsql
376
+ SECURITY DEFINER
377
+ SET search_path = public
378
+ AS $$
379
+ DECLARE
380
+ v_payload jsonb := COALESCE(p_payload, '{}'::jsonb);
381
+ v_project_name text;
382
+ BEGIN
383
+ IF p_project_id IS NOT NULL THEN
384
+ IF NOT (v_payload ? 'project_id') THEN
385
+ v_payload := v_payload || jsonb_build_object('project_id', p_project_id);
386
+ END IF;
387
+
388
+ IF NOT (v_payload ? 'project_name') THEN
389
+ v_project_name := public.project_activity_project_name(p_project_id);
390
+ IF v_project_name IS NOT NULL THEN
391
+ v_payload := v_payload || jsonb_build_object('project_name', v_project_name);
392
+ END IF;
393
+ END IF;
394
+ END IF;
395
+
396
+ PERFORM public.log_app_activity_event(
397
+ 'projects',
398
+ p_event_type,
399
+ p_entity_table,
400
+ p_entity_id,
401
+ p_summary,
402
+ v_payload,
403
+ p_actor_profile_id
404
+ );
405
+ END;
406
+ $$;
407
+
408
+ REVOKE ALL ON FUNCTION public.log_project_activity_event(text, text, uuid, uuid, text, jsonb, uuid)
409
+ FROM PUBLIC;
410
+ REVOKE ALL ON FUNCTION public.log_project_activity_event(text, text, uuid, uuid, text, jsonb, uuid)
411
+ FROM anon;
412
+ REVOKE ALL ON FUNCTION public.log_project_activity_event(text, text, uuid, uuid, text, jsonb, uuid)
413
+ FROM authenticated;
414
+ GRANT EXECUTE ON FUNCTION public.log_project_activity_event(text, text, uuid, uuid, text, jsonb, uuid)
415
+ TO service_role;
416
+
417
+ CREATE OR REPLACE FUNCTION public.log_projects_activity_event()
418
+ RETURNS trigger
419
+ LANGUAGE plpgsql
420
+ SECURITY DEFINER
421
+ SET search_path = public
422
+ AS $$
423
+ DECLARE
424
+ v_summary text;
425
+ v_changes jsonb := '{}'::jsonb;
426
+ BEGIN
427
+ IF TG_OP = 'INSERT' THEN
428
+ PERFORM public.log_project_activity_event(
429
+ 'project_created',
430
+ 'projects',
431
+ NEW.id,
432
+ NEW.id,
433
+ 'Projeto criado: ' || COALESCE(NULLIF(btrim(NEW.name), ''), 'Sem nome'),
434
+ jsonb_build_object(
435
+ 'organization_id', NEW.organization_id,
436
+ 'status', NEW.status,
437
+ 'health', NEW.health
438
+ )
439
+ );
440
+
441
+ RETURN NEW;
442
+ END IF;
443
+
444
+ IF TG_OP = 'UPDATE' THEN
445
+ IF NEW.status IS DISTINCT FROM OLD.status THEN
446
+ INSERT INTO public.project_status_log (
447
+ project_id,
448
+ previous_status,
449
+ new_status,
450
+ reason,
451
+ changed_by_profile_id
452
+ ) VALUES (
453
+ NEW.id,
454
+ OLD.status,
455
+ NEW.status,
456
+ NULL,
457
+ public.current_profile_id()
458
+ );
459
+ END IF;
460
+
461
+ IF NEW.name IS DISTINCT FROM OLD.name THEN
462
+ v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('from', OLD.name, 'to', NEW.name));
463
+ END IF;
464
+
465
+ IF NEW.status IS DISTINCT FROM OLD.status THEN
466
+ v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('from', OLD.status, 'to', NEW.status));
467
+ END IF;
468
+
469
+ IF NEW.target_date IS DISTINCT FROM OLD.target_date THEN
470
+ v_changes := v_changes || jsonb_build_object('target_date', jsonb_build_object('from', OLD.target_date, 'to', NEW.target_date));
471
+ END IF;
472
+
473
+ IF NEW.owner_profile_id IS DISTINCT FROM OLD.owner_profile_id THEN
474
+ v_changes := v_changes || jsonb_build_object(
475
+ 'owner_profile_id',
476
+ jsonb_build_object('from', OLD.owner_profile_id, 'to', NEW.owner_profile_id)
477
+ );
478
+ END IF;
479
+
480
+ IF NEW.summary IS DISTINCT FROM OLD.summary THEN
481
+ v_changes := v_changes || jsonb_build_object('summary', jsonb_build_object('from', OLD.summary, 'to', NEW.summary));
482
+ END IF;
483
+
484
+ IF NEW.name IS DISTINCT FROM OLD.name
485
+ OR NEW.summary IS DISTINCT FROM OLD.summary
486
+ OR NEW.target_date IS DISTINCT FROM OLD.target_date
487
+ OR NEW.health IS DISTINCT FROM OLD.health
488
+ OR NEW.owner_profile_id IS DISTINCT FROM OLD.owner_profile_id
489
+ OR NEW.status IS DISTINCT FROM OLD.status THEN
490
+ v_summary := 'Projeto atualizado: ' || COALESCE(NULLIF(btrim(NEW.name), ''), 'Sem nome');
491
+
492
+ PERFORM public.log_project_activity_event(
493
+ CASE WHEN NEW.status IS DISTINCT FROM OLD.status THEN 'project_status_changed' ELSE 'project_updated' END,
494
+ 'projects',
495
+ NEW.id,
496
+ NEW.id,
497
+ v_summary,
498
+ jsonb_strip_nulls(
499
+ jsonb_build_object(
500
+ 'previous_status', OLD.status,
501
+ 'new_status', NEW.status,
502
+ 'previous_health', OLD.health,
503
+ 'new_health', NEW.health,
504
+ 'changes', CASE WHEN v_changes = '{}'::jsonb THEN NULL ELSE v_changes END
505
+ )
506
+ )
507
+ );
508
+ END IF;
509
+
510
+ RETURN NEW;
511
+ END IF;
512
+
513
+ RETURN NULL;
514
+ END;
515
+ $$;
516
+
517
+ CREATE OR REPLACE FUNCTION public.log_project_tasks_activity_event()
518
+ RETURNS trigger
519
+ LANGUAGE plpgsql
520
+ SECURITY DEFINER
521
+ SET search_path = public
522
+ AS $$
523
+ DECLARE
524
+ v_changes jsonb := '{}'::jsonb;
525
+ BEGIN
526
+ IF TG_OP = 'INSERT' THEN
527
+ PERFORM public.log_project_activity_event(
528
+ 'task_created',
529
+ 'project_tasks',
530
+ NEW.id,
531
+ NEW.project_id,
532
+ 'Tarefa criada: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título'),
533
+ jsonb_build_object(
534
+ 'task_id', NEW.id,
535
+ 'status', NEW.status,
536
+ 'priority', NEW.priority,
537
+ 'assignee_profile_id', NEW.assignee_profile_id,
538
+ 'due_date', NEW.due_date
539
+ )
540
+ );
541
+
542
+ RETURN NEW;
543
+ END IF;
544
+
545
+ IF TG_OP = 'UPDATE' THEN
546
+ IF NEW.title IS DISTINCT FROM OLD.title THEN
547
+ v_changes := v_changes || jsonb_build_object('title', jsonb_build_object('from', OLD.title, 'to', NEW.title));
548
+ END IF;
549
+
550
+ IF NEW.description IS DISTINCT FROM OLD.description THEN
551
+ v_changes := v_changes || jsonb_build_object('description', jsonb_build_object('from', OLD.description, 'to', NEW.description));
552
+ END IF;
553
+
554
+ IF NEW.status IS DISTINCT FROM OLD.status THEN
555
+ v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('from', OLD.status, 'to', NEW.status));
556
+ END IF;
557
+
558
+ IF NEW.priority IS DISTINCT FROM OLD.priority THEN
559
+ v_changes := v_changes || jsonb_build_object('priority', jsonb_build_object('from', OLD.priority, 'to', NEW.priority));
560
+ END IF;
561
+
562
+ IF NEW.due_date IS DISTINCT FROM OLD.due_date THEN
563
+ v_changes := v_changes || jsonb_build_object('due_date', jsonb_build_object('from', OLD.due_date, 'to', NEW.due_date));
564
+ END IF;
565
+
566
+ IF NEW.assignee_profile_id IS DISTINCT FROM OLD.assignee_profile_id THEN
567
+ v_changes := v_changes || jsonb_build_object(
568
+ 'assignee_profile_id',
569
+ jsonb_build_object('from', OLD.assignee_profile_id, 'to', NEW.assignee_profile_id)
570
+ );
571
+ END IF;
572
+
573
+ IF NEW.milestone_id IS DISTINCT FROM OLD.milestone_id THEN
574
+ v_changes := v_changes || jsonb_build_object('milestone_id', jsonb_build_object('from', OLD.milestone_id, 'to', NEW.milestone_id));
575
+ END IF;
576
+
577
+ IF NEW.blocked_reason IS DISTINCT FROM OLD.blocked_reason THEN
578
+ v_changes := v_changes || jsonb_build_object('blocked_reason', jsonb_build_object('from', OLD.blocked_reason, 'to', NEW.blocked_reason));
579
+ END IF;
580
+
581
+ IF v_changes <> '{}'::jsonb THEN
582
+ PERFORM public.log_project_activity_event(
583
+ CASE WHEN NEW.status IS DISTINCT FROM OLD.status THEN 'task_status_changed' ELSE 'task_updated' END,
584
+ 'project_tasks',
585
+ NEW.id,
586
+ NEW.project_id,
587
+ CASE
588
+ WHEN NEW.status IS DISTINCT FROM OLD.status
589
+ THEN 'Estado da tarefa alterado: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título')
590
+ ELSE 'Tarefa atualizada: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título')
591
+ END,
592
+ jsonb_build_object(
593
+ 'task_id', NEW.id,
594
+ 'previous_status', OLD.status,
595
+ 'new_status', NEW.status,
596
+ 'changes', v_changes
597
+ )
598
+ );
599
+ END IF;
600
+
601
+ RETURN NEW;
602
+ END IF;
603
+
604
+ IF TG_OP = 'DELETE' THEN
605
+ PERFORM public.log_project_activity_event(
606
+ 'task_deleted',
607
+ 'project_tasks',
608
+ OLD.id,
609
+ OLD.project_id,
610
+ 'Tarefa eliminada: ' || COALESCE(NULLIF(btrim(OLD.title), ''), 'Sem título'),
611
+ jsonb_build_object(
612
+ 'task_id', OLD.id,
613
+ 'status', OLD.status,
614
+ 'priority', OLD.priority,
615
+ 'assignee_profile_id', OLD.assignee_profile_id,
616
+ 'due_date', OLD.due_date
617
+ )
618
+ );
619
+
620
+ RETURN OLD;
621
+ END IF;
622
+
623
+ RETURN NULL;
624
+ END;
625
+ $$;
626
+
627
+ CREATE OR REPLACE FUNCTION public.log_project_milestones_activity_event()
628
+ RETURNS trigger
629
+ LANGUAGE plpgsql
630
+ SECURITY DEFINER
631
+ SET search_path = public
632
+ AS $$
633
+ DECLARE
634
+ v_changes jsonb := '{}'::jsonb;
635
+ BEGIN
636
+ IF TG_OP = 'INSERT' THEN
637
+ PERFORM public.log_project_activity_event(
638
+ 'milestone_created',
639
+ 'project_milestones',
640
+ NEW.id,
641
+ NEW.project_id,
642
+ 'Milestone criada: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título'),
643
+ jsonb_build_object(
644
+ 'milestone_id', NEW.id,
645
+ 'status', NEW.status,
646
+ 'target_date', NEW.target_date
647
+ )
648
+ );
649
+
650
+ RETURN NEW;
651
+ END IF;
652
+
653
+ IF TG_OP = 'UPDATE' THEN
654
+ IF NEW.title IS DISTINCT FROM OLD.title THEN
655
+ v_changes := v_changes || jsonb_build_object('title', jsonb_build_object('from', OLD.title, 'to', NEW.title));
656
+ END IF;
657
+
658
+ IF NEW.status IS DISTINCT FROM OLD.status THEN
659
+ v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('from', OLD.status, 'to', NEW.status));
660
+ END IF;
661
+
662
+ IF NEW.target_date IS DISTINCT FROM OLD.target_date THEN
663
+ v_changes := v_changes || jsonb_build_object('target_date', jsonb_build_object('from', OLD.target_date, 'to', NEW.target_date));
664
+ END IF;
665
+
666
+ IF NEW.completed_at IS DISTINCT FROM OLD.completed_at THEN
667
+ v_changes := v_changes || jsonb_build_object('completed_at', jsonb_build_object('from', OLD.completed_at, 'to', NEW.completed_at));
668
+ END IF;
669
+
670
+ IF v_changes <> '{}'::jsonb THEN
671
+ PERFORM public.log_project_activity_event(
672
+ CASE WHEN NEW.status IS DISTINCT FROM OLD.status THEN 'milestone_status_changed' ELSE 'milestone_updated' END,
673
+ 'project_milestones',
674
+ NEW.id,
675
+ NEW.project_id,
676
+ CASE
677
+ WHEN NEW.status IS DISTINCT FROM OLD.status
678
+ THEN 'Estado da milestone alterado: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título')
679
+ ELSE 'Milestone atualizada: ' || COALESCE(NULLIF(btrim(NEW.title), ''), 'Sem título')
680
+ END,
681
+ jsonb_build_object(
682
+ 'milestone_id', NEW.id,
683
+ 'previous_status', OLD.status,
684
+ 'new_status', NEW.status,
685
+ 'changes', v_changes
686
+ )
687
+ );
688
+ END IF;
689
+
690
+ RETURN NEW;
691
+ END IF;
692
+
693
+ IF TG_OP = 'DELETE' THEN
694
+ PERFORM public.log_project_activity_event(
695
+ 'milestone_deleted',
696
+ 'project_milestones',
697
+ OLD.id,
698
+ OLD.project_id,
699
+ 'Milestone eliminada: ' || COALESCE(NULLIF(btrim(OLD.title), ''), 'Sem título'),
700
+ jsonb_build_object(
701
+ 'milestone_id', OLD.id,
702
+ 'status', OLD.status,
703
+ 'target_date', OLD.target_date
704
+ )
705
+ );
706
+
707
+ RETURN OLD;
708
+ END IF;
709
+
710
+ RETURN NULL;
711
+ END;
712
+ $$;
713
+
714
+ CREATE OR REPLACE FUNCTION public.log_project_links_activity_event()
715
+ RETURNS trigger
716
+ LANGUAGE plpgsql
717
+ SECURITY DEFINER
718
+ SET search_path = public
719
+ AS $$
720
+ DECLARE
721
+ v_changes jsonb := '{}'::jsonb;
722
+ BEGIN
723
+ IF TG_OP = 'INSERT' THEN
724
+ PERFORM public.log_project_activity_event(
725
+ 'link_created',
726
+ 'project_links',
727
+ NEW.id,
728
+ NEW.project_id,
729
+ 'Link criado: ' || COALESCE(NULLIF(btrim(NEW.label), ''), 'Sem rótulo'),
730
+ jsonb_build_object(
731
+ 'link_id', NEW.id,
732
+ 'kind', NEW.kind,
733
+ 'visibility', NEW.visibility
734
+ )
735
+ );
736
+
737
+ RETURN NEW;
738
+ END IF;
739
+
740
+ IF TG_OP = 'UPDATE' THEN
741
+ IF NEW.label IS DISTINCT FROM OLD.label THEN
742
+ v_changes := v_changes || jsonb_build_object('label', jsonb_build_object('from', OLD.label, 'to', NEW.label));
743
+ END IF;
744
+
745
+ IF NEW.url IS DISTINCT FROM OLD.url THEN
746
+ v_changes := v_changes || jsonb_build_object('url', jsonb_build_object('from', OLD.url, 'to', NEW.url));
747
+ END IF;
748
+
749
+ IF NEW.kind IS DISTINCT FROM OLD.kind THEN
750
+ v_changes := v_changes || jsonb_build_object('kind', jsonb_build_object('from', OLD.kind, 'to', NEW.kind));
751
+ END IF;
752
+
753
+ IF NEW.visibility IS DISTINCT FROM OLD.visibility THEN
754
+ v_changes := v_changes || jsonb_build_object('visibility', jsonb_build_object('from', OLD.visibility, 'to', NEW.visibility));
755
+ END IF;
756
+
757
+ IF v_changes <> '{}'::jsonb THEN
758
+ PERFORM public.log_project_activity_event(
759
+ 'link_updated',
760
+ 'project_links',
761
+ NEW.id,
762
+ NEW.project_id,
763
+ 'Link atualizado: ' || COALESCE(NULLIF(btrim(NEW.label), ''), 'Sem rótulo'),
764
+ jsonb_build_object(
765
+ 'link_id', NEW.id,
766
+ 'changes', v_changes
767
+ )
768
+ );
769
+ END IF;
770
+
771
+ RETURN NEW;
772
+ END IF;
773
+
774
+ IF TG_OP = 'DELETE' THEN
775
+ PERFORM public.log_project_activity_event(
776
+ 'link_deleted',
777
+ 'project_links',
778
+ OLD.id,
779
+ OLD.project_id,
780
+ 'Link eliminado: ' || COALESCE(NULLIF(btrim(OLD.label), ''), 'Sem rótulo'),
781
+ jsonb_build_object(
782
+ 'link_id', OLD.id,
783
+ 'kind', OLD.kind,
784
+ 'visibility', OLD.visibility
785
+ )
786
+ );
787
+
788
+ RETURN OLD;
789
+ END IF;
790
+
791
+ RETURN NULL;
792
+ END;
793
+ $$;
794
+
795
+ CREATE OR REPLACE FUNCTION public.log_project_members_activity_event()
796
+ RETURNS trigger
797
+ LANGUAGE plpgsql
798
+ SECURITY DEFINER
799
+ SET search_path = public
800
+ AS $$
801
+ BEGIN
802
+ IF TG_OP = 'INSERT' THEN
803
+ PERFORM public.log_project_activity_event(
804
+ 'member_added',
805
+ 'project_members',
806
+ NEW.id,
807
+ NEW.project_id,
808
+ 'Membro adicionado à equipa do projeto',
809
+ jsonb_build_object(
810
+ 'member_id', NEW.id,
811
+ 'profile_id', NEW.profile_id,
812
+ 'role', NEW.role
813
+ )
814
+ );
815
+
816
+ RETURN NEW;
817
+ END IF;
818
+
819
+ IF TG_OP = 'UPDATE' THEN
820
+ IF NEW.role IS DISTINCT FROM OLD.role THEN
821
+ PERFORM public.log_project_activity_event(
822
+ 'member_role_changed',
823
+ 'project_members',
824
+ NEW.id,
825
+ NEW.project_id,
826
+ 'Função de membro atualizada na equipa do projeto',
827
+ jsonb_build_object(
828
+ 'member_id', NEW.id,
829
+ 'profile_id', NEW.profile_id,
830
+ 'previous_role', OLD.role,
831
+ 'new_role', NEW.role,
832
+ 'changes', jsonb_build_object(
833
+ 'role', jsonb_build_object('from', OLD.role, 'to', NEW.role)
834
+ )
835
+ )
836
+ );
837
+ END IF;
838
+
839
+ RETURN NEW;
840
+ END IF;
841
+
842
+ IF TG_OP = 'DELETE' THEN
843
+ PERFORM public.log_project_activity_event(
844
+ 'member_removed',
845
+ 'project_members',
846
+ OLD.id,
847
+ OLD.project_id,
848
+ 'Membro removido da equipa do projeto',
849
+ jsonb_build_object(
850
+ 'member_id', OLD.id,
851
+ 'profile_id', OLD.profile_id,
852
+ 'role', OLD.role
853
+ )
854
+ );
855
+
856
+ RETURN OLD;
857
+ END IF;
858
+
859
+ RETURN NULL;
860
+ END;
861
+ $$;
862
+
863
+ DROP TRIGGER IF EXISTS trg_log_projects_activity_event ON public.projects;
864
+ CREATE TRIGGER trg_log_projects_activity_event
865
+ AFTER INSERT OR UPDATE ON public.projects
866
+ FOR EACH ROW
867
+ EXECUTE FUNCTION public.log_projects_activity_event();
868
+
869
+ DROP TRIGGER IF EXISTS trg_log_project_tasks_activity_event ON public.project_tasks;
870
+ CREATE TRIGGER trg_log_project_tasks_activity_event
871
+ AFTER INSERT OR UPDATE OR DELETE ON public.project_tasks
872
+ FOR EACH ROW
873
+ EXECUTE FUNCTION public.log_project_tasks_activity_event();
874
+
875
+ DROP TRIGGER IF EXISTS trg_log_project_milestones_activity_event ON public.project_milestones;
876
+ CREATE TRIGGER trg_log_project_milestones_activity_event
877
+ AFTER INSERT OR UPDATE OR DELETE ON public.project_milestones
878
+ FOR EACH ROW
879
+ EXECUTE FUNCTION public.log_project_milestones_activity_event();
880
+
881
+ DROP TRIGGER IF EXISTS trg_log_project_links_activity_event ON public.project_links;
882
+ CREATE TRIGGER trg_log_project_links_activity_event
883
+ AFTER INSERT OR UPDATE OR DELETE ON public.project_links
884
+ FOR EACH ROW
885
+ EXECUTE FUNCTION public.log_project_links_activity_event();
886
+
887
+ DROP TRIGGER IF EXISTS trg_log_project_members_activity_event ON public.project_members;
888
+ CREATE TRIGGER trg_log_project_members_activity_event
889
+ AFTER INSERT OR UPDATE OR DELETE ON public.project_members
890
+ FOR EACH ROW
891
+ EXECUTE FUNCTION public.log_project_members_activity_event();
892
+
893
+ CREATE OR REPLACE FUNCTION public.sync_org_primary_contact_admin_membership()
894
+ RETURNS trigger
895
+ LANGUAGE plpgsql
896
+ SECURITY DEFINER
897
+ SET search_path = public
898
+ AS $$
899
+ BEGIN
900
+ IF TG_OP = 'UPDATE' AND OLD.primary_contact_id IS DISTINCT FROM NEW.primary_contact_id THEN
901
+ IF OLD.primary_contact_id IS NOT NULL THEN
902
+ DELETE FROM public.organization_members
903
+ WHERE organization_id = NEW.id
904
+ AND profile_id = OLD.primary_contact_id
905
+ AND role = 'admin';
906
+
907
+ DELETE FROM public.project_members pm
908
+ USING public.projects p
909
+ WHERE p.id = pm.project_id
910
+ AND p.organization_id = NEW.id
911
+ AND pm.profile_id = OLD.primary_contact_id
912
+ AND pm.role = 'observer';
913
+ END IF;
914
+ END IF;
915
+
916
+ IF NEW.primary_contact_id IS NOT NULL THEN
917
+ INSERT INTO public.organization_members (organization_id, profile_id, role)
918
+ VALUES (NEW.id, NEW.primary_contact_id, 'admin')
919
+ ON CONFLICT (organization_id, profile_id)
920
+ DO UPDATE SET role = 'admin';
921
+
922
+ INSERT INTO public.project_members (project_id, profile_id, role)
923
+ SELECT p.id, NEW.primary_contact_id, 'observer'
924
+ FROM public.projects p
925
+ WHERE p.organization_id = NEW.id
926
+ ON CONFLICT (project_id, profile_id)
927
+ DO NOTHING;
928
+ END IF;
929
+
930
+ RETURN NEW;
931
+ END;
932
+ $$;
933
+
934
+ DROP TRIGGER IF EXISTS trg_sync_org_primary_contact_admin_membership ON public.organizations;
935
+ CREATE TRIGGER trg_sync_org_primary_contact_admin_membership
936
+ AFTER INSERT OR UPDATE OF primary_contact_id ON public.organizations
937
+ FOR EACH ROW
938
+ EXECUTE FUNCTION public.sync_org_primary_contact_admin_membership();
939
+
940
+ ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
941
+ ALTER TABLE public.project_members ENABLE ROW LEVEL SECURITY;
942
+ ALTER TABLE public.project_milestones ENABLE ROW LEVEL SECURITY;
943
+ ALTER TABLE public.project_tasks ENABLE ROW LEVEL SECURITY;
944
+ ALTER TABLE public.project_links ENABLE ROW LEVEL SECURITY;
945
+ ALTER TABLE public.project_status_log ENABLE ROW LEVEL SECURITY;
946
+
947
+ DROP POLICY IF EXISTS "Project team view projects" ON public.projects;
948
+ CREATE POLICY "Project team view projects"
949
+ ON public.projects
950
+ FOR SELECT
951
+ TO authenticated
952
+ USING (
953
+ public.is_admin()
954
+ OR public.is_project_team_member(id)
955
+ OR public.is_project_org_admin(id)
956
+ );
957
+
958
+ DROP POLICY IF EXISTS "Project team create projects" ON public.projects;
959
+ CREATE POLICY "Project team create projects"
960
+ ON public.projects
961
+ FOR INSERT
962
+ TO authenticated
963
+ WITH CHECK (
964
+ public.is_admin()
965
+ OR owner_profile_id = public.current_profile_id()
966
+ OR public.can_create_projects_for_org(organization_id)
967
+ );
968
+
969
+ DROP POLICY IF EXISTS "Project team update projects" ON public.projects;
970
+ CREATE POLICY "Project team update projects"
971
+ ON public.projects
972
+ FOR UPDATE
973
+ TO authenticated
974
+ USING (public.is_admin() OR public.is_project_content_editor(id))
975
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(id));
976
+
977
+ DROP POLICY IF EXISTS "Project owners delete projects" ON public.projects;
978
+ CREATE POLICY "Project owners delete projects"
979
+ ON public.projects
980
+ FOR DELETE
981
+ TO authenticated
982
+ USING (public.is_admin() OR public.is_project_owner_member(id));
983
+
984
+ DROP POLICY IF EXISTS "Project team view project members" ON public.project_members;
985
+ CREATE POLICY "Project team view project members"
986
+ ON public.project_members
987
+ FOR SELECT
988
+ TO authenticated
989
+ USING (public.is_admin() OR public.is_project_team_member(project_id));
990
+
991
+ DROP POLICY IF EXISTS "Project owners insert project members" ON public.project_members;
992
+ CREATE POLICY "Project owners insert project members"
993
+ ON public.project_members
994
+ FOR INSERT
995
+ TO authenticated
996
+ WITH CHECK (public.is_admin() OR public.is_project_owner_member(project_id));
997
+
998
+ DROP POLICY IF EXISTS "Project owners update project members" ON public.project_members;
999
+ CREATE POLICY "Project owners update project members"
1000
+ ON public.project_members
1001
+ FOR UPDATE
1002
+ TO authenticated
1003
+ USING (public.is_admin() OR public.is_project_owner_member(project_id))
1004
+ WITH CHECK (public.is_admin() OR public.is_project_owner_member(project_id));
1005
+
1006
+ DROP POLICY IF EXISTS "Project owners delete project members" ON public.project_members;
1007
+ CREATE POLICY "Project owners delete project members"
1008
+ ON public.project_members
1009
+ FOR DELETE
1010
+ TO authenticated
1011
+ USING (public.is_admin() OR public.is_project_owner_member(project_id));
1012
+
1013
+ DROP POLICY IF EXISTS "Project team view project milestones" ON public.project_milestones;
1014
+ CREATE POLICY "Project team view project milestones"
1015
+ ON public.project_milestones
1016
+ FOR SELECT
1017
+ TO authenticated
1018
+ USING (public.is_admin() OR public.is_project_team_member(project_id));
1019
+
1020
+ DROP POLICY IF EXISTS "Project content editors insert project milestones" ON public.project_milestones;
1021
+ CREATE POLICY "Project content editors insert project milestones"
1022
+ ON public.project_milestones
1023
+ FOR INSERT
1024
+ TO authenticated
1025
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(project_id));
1026
+
1027
+ DROP POLICY IF EXISTS "Project content editors update project milestones" ON public.project_milestones;
1028
+ CREATE POLICY "Project content editors update project milestones"
1029
+ ON public.project_milestones
1030
+ FOR UPDATE
1031
+ TO authenticated
1032
+ USING (public.is_admin() OR public.is_project_content_editor(project_id))
1033
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(project_id));
1034
+
1035
+ DROP POLICY IF EXISTS "Project content editors delete project milestones" ON public.project_milestones;
1036
+ CREATE POLICY "Project content editors delete project milestones"
1037
+ ON public.project_milestones
1038
+ FOR DELETE
1039
+ TO authenticated
1040
+ USING (public.is_admin() OR public.is_project_content_editor(project_id));
1041
+
1042
+ DROP POLICY IF EXISTS "Project team view project tasks" ON public.project_tasks;
1043
+ CREATE POLICY "Project team view project tasks"
1044
+ ON public.project_tasks
1045
+ FOR SELECT
1046
+ TO authenticated
1047
+ USING (public.is_admin() OR public.is_project_team_member(project_id));
1048
+
1049
+ DROP POLICY IF EXISTS "Project content editors insert project tasks" ON public.project_tasks;
1050
+ CREATE POLICY "Project content editors insert project tasks"
1051
+ ON public.project_tasks
1052
+ FOR INSERT
1053
+ TO authenticated
1054
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(project_id));
1055
+
1056
+ DROP POLICY IF EXISTS "Project content editors update project tasks" ON public.project_tasks;
1057
+ CREATE POLICY "Project content editors update project tasks"
1058
+ ON public.project_tasks
1059
+ FOR UPDATE
1060
+ TO authenticated
1061
+ USING (public.is_admin() OR public.is_project_content_editor(project_id))
1062
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(project_id));
1063
+
1064
+ DROP POLICY IF EXISTS "Project content editors delete project tasks" ON public.project_tasks;
1065
+ CREATE POLICY "Project content editors delete project tasks"
1066
+ ON public.project_tasks
1067
+ FOR DELETE
1068
+ TO authenticated
1069
+ USING (public.is_admin() OR public.is_project_content_editor(project_id));
1070
+
1071
+ DROP POLICY IF EXISTS "Project team view project links" ON public.project_links;
1072
+ CREATE POLICY "Project team view project links"
1073
+ ON public.project_links
1074
+ FOR SELECT
1075
+ TO authenticated
1076
+ USING (public.is_admin() OR public.is_project_team_member(project_id));
1077
+
1078
+ DROP POLICY IF EXISTS "Project team create project links" ON public.project_links;
1079
+ CREATE POLICY "Project team create project links"
1080
+ ON public.project_links
1081
+ FOR INSERT
1082
+ TO authenticated
1083
+ WITH CHECK (public.is_admin() OR public.is_project_team_member(project_id));
1084
+
1085
+ DROP POLICY IF EXISTS "Project content editors update project links" ON public.project_links;
1086
+ CREATE POLICY "Project content editors update project links"
1087
+ ON public.project_links
1088
+ FOR UPDATE
1089
+ TO authenticated
1090
+ USING (public.is_admin() OR public.is_project_content_editor(project_id))
1091
+ WITH CHECK (public.is_admin() OR public.is_project_content_editor(project_id));
1092
+
1093
+ DROP POLICY IF EXISTS "Project content editors delete project links" ON public.project_links;
1094
+ CREATE POLICY "Project content editors delete project links"
1095
+ ON public.project_links
1096
+ FOR DELETE
1097
+ TO authenticated
1098
+ USING (public.is_admin() OR public.is_project_content_editor(project_id));
1099
+
1100
+ DROP POLICY IF EXISTS "Project team view project status log" ON public.project_status_log;
1101
+ CREATE POLICY "Project team view project status log"
1102
+ ON public.project_status_log
1103
+ FOR SELECT
1104
+ TO authenticated
1105
+ USING (public.is_admin() OR public.is_project_team_member(project_id));
1106
+
1107
+ INSERT INTO public.organization_members (organization_id, profile_id, role)
1108
+ SELECT o.id, o.primary_contact_id, 'admin'
1109
+ FROM public.organizations o
1110
+ WHERE o.primary_contact_id IS NOT NULL
1111
+ ON CONFLICT (organization_id, profile_id)
1112
+ DO UPDATE SET role = 'admin';
1113
+
1114
+ INSERT INTO public.project_members (project_id, profile_id, role)
1115
+ SELECT p.id, o.primary_contact_id, 'observer'
1116
+ FROM public.projects p
1117
+ JOIN public.organizations o ON o.id = p.organization_id
1118
+ WHERE o.primary_contact_id IS NOT NULL
1119
+ ON CONFLICT (project_id, profile_id)
1120
+ DO NOTHING;