create-bw-app 0.9.4 → 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 +5 -0
- package/package.json +1 -1
- package/src/cli.mjs +5 -0
- package/src/constants.mjs +1 -0
- package/src/generator.mjs +102 -36
- package/src/update.mjs +67 -1
- package/template/base/AGENTS.md +1 -0
- package/template/base/app/playground/auth/page.tsx +1 -1
- package/template/base/app/preview/app-shell/page.tsx +1 -1
- package/template/base/{app/preview → components}/app-shell-preview.tsx +2 -2
- package/template/base/docs/ai/README.md +3 -0
- package/template/base/docs/ai/examples.md +2 -0
- package/template/modules/crm/app/api/crm/_shared/create-module-route-handler.ts +13 -0
- package/template/modules/crm/app/api/crm/contacts/route.ts +6 -4
- package/template/modules/crm/app/api/crm/organizations/route.ts +6 -4
- package/template/modules/crm/app/api/crm/owners/route.ts +6 -4
- package/template/modules/crm/app/api/crm/stats/route.ts +6 -4
- 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
- /package/template/base/{app/playground/auth → components}/auth-playground.tsx +0 -0
|
@@ -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;
|