create-bw-app 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app-manifest.mjs +8 -0
- package/src/constants.mjs +14 -9
- package/src/doctor.mjs +181 -1
- package/src/generator.mjs +29 -9
- package/src/migrations.mjs +116 -6
- package/src/upgrade.mjs +35 -2
- package/template/base/app/(shell)/shell-layout-client.tsx +1 -1
- package/template/base/app/api/organizations/[id]/invitations/[invitationId]/route.ts +8 -0
- package/template/base/app/api/organizations/[id]/invitations/route.ts +2 -2
- package/template/base/app/api/organizations/[id]/route.ts +2 -2
- package/template/base/app/api/organizations/route.ts +2 -2
- package/template/base/config/module-toolbar-controls.tsx +6 -4
- package/template/modules/projects/app/(shell)/projetos/[projectId]/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/quadro/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/[projectId]/tarefas/page.tsx +1 -1
- package/template/modules/projects/app/(shell)/projetos/page.tsx +1 -1
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/account/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/activity/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/links/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/members/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +1 -1
- package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +1 -1
- package/template/modules/projects/app/api/projects/organizations/route.ts +1 -1
- package/template/modules/projects/app/api/projects/route.ts +1 -1
- package/template/modules/projects/app/api/projects/setup-options/route.ts +1 -1
- package/template/modules/projects/app/api/projects/stats/route.ts +1 -1
- package/template/supabase/modules/crm/migrations/20260816120000_crm_contact_organizations.sql +102 -0
- package/template/supabase/modules/projects/migrations/20260815120000_project_client_access_member_roles.sql +77 -0
- package/template/supabase/modules/projects/migrations/20260815133000_project_admin_creation_and_task_permissions.sql +376 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-live-mounts.tsx +0 -27
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +0 -63
- package/template/modules/projects/app/api/projects/_handlers.ts +0 -110
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
3
|
export async function PATCH(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
4
|
-
const {
|
|
5
|
-
return
|
|
4
|
+
const { handleCrmOrganizationPatchRequest } = await import("@brightweblabs/module-crm");
|
|
5
|
+
return handleCrmOrganizationPatchRequest(request, context);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export async function DELETE(request: Request, context: { params: Promise<{ id: string }> }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
2
|
|
|
3
3
|
export async function POST(request: Request) {
|
|
4
|
-
const {
|
|
5
|
-
return
|
|
4
|
+
const { handleCrmOrganizationsPostRequest } = await import("@brightweblabs/module-crm");
|
|
5
|
+
return handleCrmOrganizationsPostRequest(request);
|
|
6
6
|
}
|
|
@@ -8,16 +8,18 @@ import { ProjectBoardToolbarControls, ProjectsToolbarControls } from "@brightweb
|
|
|
8
8
|
import { MarketingToolbarControls } from "@brightweblabs/module-marketing/ui";
|
|
9
9
|
|
|
10
10
|
// MANAGED BY BRIGHTWEB — regenerated when modules are added, removed, or updated.
|
|
11
|
-
|
|
11
|
+
type ModuleToolbarViewer = { isAdmin: boolean };
|
|
12
|
+
|
|
13
|
+
const toolbarControlBySurface: Partial<Record<ShellToolbarSurface, (viewer: ModuleToolbarViewer) => ReactNode>> = {
|
|
12
14
|
"admin-users": () => <AdminToolbarControls />,
|
|
13
15
|
crm: () => <CrmToolbarControls />,
|
|
14
16
|
"crm-organizations": () => <CrmOrganizationsToolbarControls />,
|
|
15
|
-
projects: () => <ProjectsToolbarControls />,
|
|
17
|
+
projects: (viewer) => <ProjectsToolbarControls viewer={viewer} />,
|
|
16
18
|
"project-board": () => <ProjectBoardToolbarControls />,
|
|
17
19
|
marketing: () => <MarketingToolbarControls />,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[]) {
|
|
22
|
+
export function getModuleToolbarControls(pathname: string, toolbarRoutes: ShellToolbarRouteConfig[], viewer: ModuleToolbarViewer) {
|
|
21
23
|
const surface = resolveShellToolbarSurface(pathname, toolbarRoutes);
|
|
22
|
-
return surface ? toolbarControlBySurface[surface]?.() ?? null : null;
|
|
24
|
+
return surface ? toolbarControlBySurface[surface]?.(viewer) ?? null : null;
|
|
23
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectDetailPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectBoardPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectTasksPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ProjectsPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { clientProjectDetailGet as GET } from "
|
|
2
|
+
export { clientProjectDetailGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { clientProjectsGet as GET } from "
|
|
2
|
+
export { clientProjectsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectActivityGet as GET } from "
|
|
2
|
+
export { projectActivityGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectClientAccessGet as GET, projectClientAccessPatch as PATCH } from "
|
|
2
|
+
export { projectClientAccessGet as GET, projectClientAccessPatch as PATCH } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectLinkPatch as PATCH, projectLinkDelete as DELETE } from "
|
|
2
|
+
export { projectLinkPatch as PATCH, projectLinkDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectLinksPost as POST } from "
|
|
2
|
+
export { projectLinksPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMembersGet as GET, projectMembersPut as PUT } from "
|
|
2
|
+
export { projectMembersGet as GET, projectMembersPut as PUT } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMilestonePatch as PATCH, projectMilestoneDelete as DELETE } from "
|
|
2
|
+
export { projectMilestonePatch as PATCH, projectMilestoneDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectMilestonesPost as POST } from "
|
|
2
|
+
export { projectMilestonesPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectOrganizationsPatch as PATCH } from "
|
|
2
|
+
export { projectOrganizationsPatch as PATCH } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectDashboardGet as GET, projectPatch as PATCH, projectDelete as DELETE } from "
|
|
2
|
+
export { projectDashboardGet as GET, projectPatch as PATCH, projectDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectTaskPatch as PATCH, projectTaskDelete as DELETE } from "
|
|
2
|
+
export { projectTaskPatch as PATCH, projectTaskDelete as DELETE } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectTasksPost as POST } from "
|
|
2
|
+
export { projectTasksPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsOrganizationsGet as GET, projectsOrganizationsPost as POST } from "
|
|
2
|
+
export { projectsOrganizationsGet as GET, projectsOrganizationsPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsGet as GET, projectsPost as POST } from "
|
|
2
|
+
export { projectsGet as GET, projectsPost as POST } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsSetupOptionsGet as GET } from "
|
|
2
|
+
export { projectsSetupOptionsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const dynamic = "force-dynamic";
|
|
2
|
-
export { projectsStatsGet as GET } from "
|
|
2
|
+
export { projectsStatsGet as GET } from "@brightweblabs/module-projects/route-handlers";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- Compatibility-first multi-organization CRM contacts.
|
|
2
|
+
-- crm_contacts.organization_id remains the primary organization projection while
|
|
3
|
+
-- this link table records every organization relationship.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS public.crm_contact_organizations (
|
|
6
|
+
contact_id uuid NOT NULL REFERENCES public.crm_contacts(id) ON DELETE CASCADE,
|
|
7
|
+
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
|
8
|
+
is_primary boolean NOT NULL DEFAULT false,
|
|
9
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
10
|
+
PRIMARY KEY (contact_id, organization_id)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE UNIQUE INDEX IF NOT EXISTS crm_contact_organizations_one_primary_idx
|
|
14
|
+
ON public.crm_contact_organizations (contact_id)
|
|
15
|
+
WHERE is_primary;
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS crm_contact_organizations_organization_idx
|
|
18
|
+
ON public.crm_contact_organizations (organization_id, contact_id);
|
|
19
|
+
|
|
20
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
21
|
+
SELECT contact.id, contact.organization_id, true
|
|
22
|
+
FROM public.crm_contacts contact
|
|
23
|
+
WHERE contact.organization_id IS NOT NULL
|
|
24
|
+
ON CONFLICT (contact_id, organization_id) DO UPDATE SET is_primary = true;
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION public.sync_crm_contact_primary_organization_link()
|
|
27
|
+
RETURNS trigger
|
|
28
|
+
LANGUAGE plpgsql
|
|
29
|
+
SECURITY DEFINER
|
|
30
|
+
SET search_path = public
|
|
31
|
+
AS $$
|
|
32
|
+
BEGIN
|
|
33
|
+
UPDATE public.crm_contact_organizations
|
|
34
|
+
SET is_primary = false
|
|
35
|
+
WHERE contact_id = NEW.id
|
|
36
|
+
AND is_primary;
|
|
37
|
+
|
|
38
|
+
IF NEW.organization_id IS NOT NULL THEN
|
|
39
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
40
|
+
VALUES (NEW.id, NEW.organization_id, true)
|
|
41
|
+
ON CONFLICT (contact_id, organization_id)
|
|
42
|
+
DO UPDATE SET is_primary = true;
|
|
43
|
+
END IF;
|
|
44
|
+
|
|
45
|
+
RETURN NEW;
|
|
46
|
+
END;
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
DROP TRIGGER IF EXISTS trg_sync_crm_contact_primary_organization_link ON public.crm_contacts;
|
|
50
|
+
CREATE TRIGGER trg_sync_crm_contact_primary_organization_link
|
|
51
|
+
AFTER INSERT OR UPDATE OF organization_id ON public.crm_contacts
|
|
52
|
+
FOR EACH ROW
|
|
53
|
+
EXECUTE FUNCTION public.sync_crm_contact_primary_organization_link();
|
|
54
|
+
|
|
55
|
+
CREATE OR REPLACE FUNCTION public.link_crm_contact_organization(
|
|
56
|
+
p_contact_id uuid,
|
|
57
|
+
p_organization_id uuid
|
|
58
|
+
)
|
|
59
|
+
RETURNS void
|
|
60
|
+
LANGUAGE plpgsql
|
|
61
|
+
SECURITY DEFINER
|
|
62
|
+
SET search_path = public
|
|
63
|
+
AS $$
|
|
64
|
+
DECLARE
|
|
65
|
+
v_primary_organization_id uuid;
|
|
66
|
+
BEGIN
|
|
67
|
+
SELECT organization_id
|
|
68
|
+
INTO v_primary_organization_id
|
|
69
|
+
FROM public.crm_contacts
|
|
70
|
+
WHERE id = p_contact_id
|
|
71
|
+
FOR UPDATE;
|
|
72
|
+
|
|
73
|
+
IF NOT FOUND THEN
|
|
74
|
+
RAISE EXCEPTION 'CRM contact not found';
|
|
75
|
+
END IF;
|
|
76
|
+
|
|
77
|
+
IF v_primary_organization_id IS NULL THEN
|
|
78
|
+
UPDATE public.crm_contacts
|
|
79
|
+
SET organization_id = p_organization_id
|
|
80
|
+
WHERE id = p_contact_id;
|
|
81
|
+
ELSE
|
|
82
|
+
INSERT INTO public.crm_contact_organizations (contact_id, organization_id, is_primary)
|
|
83
|
+
VALUES (p_contact_id, p_organization_id, v_primary_organization_id = p_organization_id)
|
|
84
|
+
ON CONFLICT (contact_id, organization_id) DO NOTHING;
|
|
85
|
+
END IF;
|
|
86
|
+
END;
|
|
87
|
+
$$;
|
|
88
|
+
|
|
89
|
+
REVOKE ALL ON FUNCTION public.link_crm_contact_organization(uuid, uuid)
|
|
90
|
+
FROM PUBLIC, anon, authenticated;
|
|
91
|
+
GRANT EXECUTE ON FUNCTION public.link_crm_contact_organization(uuid, uuid)
|
|
92
|
+
TO service_role;
|
|
93
|
+
|
|
94
|
+
ALTER TABLE public.crm_contact_organizations ENABLE ROW LEVEL SECURITY;
|
|
95
|
+
|
|
96
|
+
DROP POLICY IF EXISTS "Staff manage crm contact organizations" ON public.crm_contact_organizations;
|
|
97
|
+
DROP POLICY IF EXISTS "Staff read crm contact organizations" ON public.crm_contact_organizations;
|
|
98
|
+
CREATE POLICY "Staff read crm contact organizations"
|
|
99
|
+
ON public.crm_contact_organizations
|
|
100
|
+
FOR SELECT
|
|
101
|
+
TO authenticated
|
|
102
|
+
USING (public.is_staff());
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_project_access_configuration(target_project_id uuid)
|
|
2
|
+
RETURNS jsonb
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
STABLE
|
|
5
|
+
SECURITY DEFINER
|
|
6
|
+
SET search_path = public
|
|
7
|
+
AS $$
|
|
8
|
+
DECLARE
|
|
9
|
+
v_result jsonb;
|
|
10
|
+
BEGIN
|
|
11
|
+
IF NOT (
|
|
12
|
+
public.is_admin()
|
|
13
|
+
OR (public.is_staff() AND public.is_project_owner_member(target_project_id))
|
|
14
|
+
) THEN
|
|
15
|
+
RAISE EXCEPTION 'Only project owners or administrators can view client access configuration.'
|
|
16
|
+
USING ERRCODE = '42501';
|
|
17
|
+
END IF;
|
|
18
|
+
|
|
19
|
+
SELECT jsonb_build_object(
|
|
20
|
+
'mode', project.client_access_mode,
|
|
21
|
+
'updated_at', project.updated_at,
|
|
22
|
+
'client_summary', project.client_summary,
|
|
23
|
+
'client_scope', project.client_scope,
|
|
24
|
+
'client_contact_profile_id', project.client_contact_profile_id,
|
|
25
|
+
'organizations', COALESCE((
|
|
26
|
+
SELECT jsonb_agg(
|
|
27
|
+
jsonb_build_object(
|
|
28
|
+
'organization_id', organization.id,
|
|
29
|
+
'organization_name', organization.name,
|
|
30
|
+
'is_primary', participant.is_primary,
|
|
31
|
+
'selected_for_client_access', audience.project_id IS NOT NULL,
|
|
32
|
+
'eligible_clients', COALESCE((
|
|
33
|
+
SELECT jsonb_agg(
|
|
34
|
+
jsonb_build_object(
|
|
35
|
+
'profile_id', profile.id,
|
|
36
|
+
'label', COALESCE(NULLIF(btrim(concat_ws(' ', profile.first_name, profile.last_name)), ''), profile.email),
|
|
37
|
+
'email', profile.email,
|
|
38
|
+
'organization_role', org_member.role
|
|
39
|
+
) ORDER BY profile.first_name, profile.last_name, profile.email
|
|
40
|
+
)
|
|
41
|
+
FROM public.organization_members org_member
|
|
42
|
+
JOIN public.profiles profile ON profile.id = org_member.profile_id AND profile.user_id IS NOT NULL
|
|
43
|
+
JOIN public.user_role_assignments role_assignment
|
|
44
|
+
ON role_assignment.profile_id = profile.id AND role_assignment.role_code = 'client'
|
|
45
|
+
WHERE org_member.organization_id = organization.id
|
|
46
|
+
), '[]'::jsonb),
|
|
47
|
+
'selected_profile_ids', COALESCE((
|
|
48
|
+
SELECT jsonb_agg(client_profile.profile_id ORDER BY client_profile.profile_id)
|
|
49
|
+
FROM public.project_client_profiles client_profile
|
|
50
|
+
WHERE client_profile.project_id = target_project_id
|
|
51
|
+
AND client_profile.organization_id = organization.id
|
|
52
|
+
), '[]'::jsonb)
|
|
53
|
+
) ORDER BY participant.is_primary DESC, organization.name
|
|
54
|
+
)
|
|
55
|
+
FROM public.project_organizations participant
|
|
56
|
+
JOIN public.organizations organization ON organization.id = participant.organization_id
|
|
57
|
+
LEFT JOIN public.project_client_organizations audience
|
|
58
|
+
ON audience.project_id = participant.project_id
|
|
59
|
+
AND audience.organization_id = participant.organization_id
|
|
60
|
+
WHERE participant.project_id = target_project_id
|
|
61
|
+
), '[]'::jsonb)
|
|
62
|
+
)
|
|
63
|
+
INTO v_result
|
|
64
|
+
FROM public.projects project
|
|
65
|
+
WHERE project.id = target_project_id;
|
|
66
|
+
|
|
67
|
+
IF v_result IS NULL THEN
|
|
68
|
+
RAISE EXCEPTION 'Projeto não encontrado.' USING ERRCODE = 'P0002';
|
|
69
|
+
END IF;
|
|
70
|
+
RETURN v_result;
|
|
71
|
+
END;
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
REVOKE ALL ON FUNCTION public.get_project_access_configuration(uuid)
|
|
75
|
+
FROM PUBLIC, anon, service_role;
|
|
76
|
+
GRANT EXECUTE ON FUNCTION public.get_project_access_configuration(uuid)
|
|
77
|
+
TO authenticated;
|