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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/app-manifest.mjs +8 -0
  3. package/src/constants.mjs +14 -9
  4. package/src/doctor.mjs +181 -1
  5. package/src/generator.mjs +29 -9
  6. package/src/migrations.mjs +116 -6
  7. package/src/upgrade.mjs +35 -2
  8. package/template/base/app/(shell)/shell-layout-client.tsx +1 -1
  9. package/template/base/app/api/organizations/[id]/invitations/[invitationId]/route.ts +8 -0
  10. package/template/base/app/api/organizations/[id]/invitations/route.ts +2 -2
  11. package/template/base/app/api/organizations/[id]/route.ts +2 -2
  12. package/template/base/app/api/organizations/route.ts +2 -2
  13. package/template/base/config/module-toolbar-controls.tsx +6 -4
  14. package/template/modules/projects/app/(shell)/projetos/[projectId]/page.tsx +1 -1
  15. package/template/modules/projects/app/(shell)/projetos/[projectId]/quadro/page.tsx +1 -1
  16. package/template/modules/projects/app/(shell)/projetos/[projectId]/tarefas/page.tsx +1 -1
  17. package/template/modules/projects/app/(shell)/projetos/page.tsx +1 -1
  18. package/template/modules/projects/app/api/account/projects/[id]/route.ts +1 -1
  19. package/template/modules/projects/app/api/account/projects/route.ts +1 -1
  20. package/template/modules/projects/app/api/projects/[id]/activity/route.ts +1 -1
  21. package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +1 -1
  22. package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +1 -1
  23. package/template/modules/projects/app/api/projects/[id]/links/route.ts +1 -1
  24. package/template/modules/projects/app/api/projects/[id]/members/route.ts +1 -1
  25. package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +1 -1
  26. package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +1 -1
  27. package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +1 -1
  28. package/template/modules/projects/app/api/projects/[id]/route.ts +1 -1
  29. package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +1 -1
  30. package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +1 -1
  31. package/template/modules/projects/app/api/projects/organizations/route.ts +1 -1
  32. package/template/modules/projects/app/api/projects/route.ts +1 -1
  33. package/template/modules/projects/app/api/projects/setup-options/route.ts +1 -1
  34. package/template/modules/projects/app/api/projects/stats/route.ts +1 -1
  35. package/template/supabase/modules/crm/migrations/20260816120000_crm_contact_organizations.sql +102 -0
  36. package/template/supabase/modules/projects/migrations/20260815120000_project_client_access_member_roles.sql +77 -0
  37. package/template/supabase/modules/projects/migrations/20260815133000_project_admin_creation_and_task_permissions.sql +376 -0
  38. package/template/modules/projects/app/(shell)/projetos/projetos-live-mounts.tsx +0 -27
  39. package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +0 -63
  40. 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 { handleOrganizationPatchRequest } = await import("@brightweblabs/module-orgs");
5
- return handleOrganizationPatchRequest(request, context);
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 { handleOrganizationsPostRequest } = await import("@brightweblabs/module-orgs");
5
- return handleOrganizationsPostRequest(request);
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
- const toolbarControlBySurface: Partial<Record<ShellToolbarSurface, () => ReactNode>> = {
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 { ProjectDetailServerMount as default } from "../projetos-server-mounts";
1
+ export { ProjectDetailPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
@@ -1 +1 @@
1
- export { ProjectBoardServerMount as default } from "../../projetos-server-mounts";
1
+ export { ProjectBoardPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
@@ -1 +1 @@
1
- export { ProjectTasksServerMount as default } from "../../projetos-server-mounts";
1
+ export { ProjectTasksPortugueseRoutePage as default } from "@brightweblabs/module-projects/routes";
@@ -1 +1 @@
1
- export { ProjectsServerMount as default } from "./projetos-server-mounts";
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 "../../../projects/_handlers";
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 "../../projects/_handlers";
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 "../../_handlers";
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 "../../_handlers";
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 "../../../_handlers";
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 "../../_handlers";
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 "../../_handlers";
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 "../../../_handlers";
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 "../../_handlers";
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 "../../_handlers";
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 "../_handlers";
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 "../../../_handlers";
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 "../../_handlers";
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 "../_handlers";
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 "./_handlers";
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 "../_handlers";
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 "../_handlers";
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;