diff --git a/CHANGELOG.md b/CHANGELOG.md index 265b8cc..07547d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,21 @@ as the sole owner of their own account and sees identical data. - **The signup trigger (`handle_new_user`) now also creates a personal account** and links the new profile to it as `owner`. +### Changed + +- **Role-aware UI gating across the app.** The inbox composer's + send button + textarea, the "New broadcast / automation / flow" + buttons, the "Add pipeline / deal" buttons, and the "Add / + Import contact" buttons are now disabled-with-tooltip for + viewers (and for agents on settings-class actions). Choice: + show-but-disable rather than hide, so the UI never feels + silently broken to a teammate looking at a feature they don't + yet have permission for. +- **Sidebar surfaces the active account** above the user info + when the `account_sharing` beta flag is on. Solo users keep + the original layout (their account is named after them, so + duplicating it would just add visual noise). + ### Fixed - **Inbound WhatsApp messages now land in the shared inbox.** The diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx index dadead5..ed861d3 100644 --- a/src/app/(dashboard)/automations/page.tsx +++ b/src/app/(dashboard)/automations/page.tsx @@ -19,6 +19,7 @@ import { } from "lucide-react" import { createClient } from "@/lib/supabase/client" +import { useCan } from "@/hooks/use-can" import type { Automation } from "@/types" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" @@ -55,8 +56,12 @@ const TEMPLATE_ICON: Record = { follow_up_reminder: PhoneCall, } +const READ_ONLY_TITLE = + "Read-only — your role can't create automations" + export default function AutomationsPage() { const router = useRouter() + const canCreate = useCan("send-messages") const [automations, setAutomations] = useState(null) const [error, setError] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) @@ -164,6 +169,8 @@ export default function AutomationsPage() { {flows.length === 0 ? ( - setCreateOpen(true)} /> + setCreateOpen(true)} + canCreate={canCreate} + /> ) : (
{flows.map((flow) => ( @@ -312,7 +323,13 @@ export default function FlowsPage() { ); } -function EmptyState({ onCreate }: { onCreate: () => void }) { +function EmptyState({ + onCreate, + canCreate, +}: { + onCreate: () => void; + canCreate: boolean; +}) { return (
@@ -326,7 +343,12 @@ function EmptyState({ onCreate }: { onCreate: () => void }) { bot. Customers tap buttons; the bot routes them to the right answer (or the right agent).

- diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 7d0a539..24a95e2 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -26,6 +26,14 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { GitBranch, Plus, ChevronDown, Settings } from "lucide-react"; import { toast } from "sonner"; +import { useCan } from "@/hooks/use-can"; + +// Pipeline creation is admin-class (it's a settings-tier write +// under the new RLS), but deal creation is operational and only +// requires agent+. The two CTAs gate on different capabilities. +const PIPELINE_READ_ONLY_TITLE = + "Read-only — your role can't create pipelines"; +const DEAL_READ_ONLY_TITLE = "Read-only — your role can't create deals"; // Spec-defined seed — name and color per the product spec. const SPEC_DEFAULT_STAGES = [ @@ -38,6 +46,8 @@ const SPEC_DEFAULT_STAGES = [ export default function PipelinesPage() { const supabase = createClient(); + const canEditSettings = useCan("edit-settings"); + const canCreateDeals = useCan("send-messages"); const [pipelines, setPipelines] = useState([]); const [selectedPipelineId, setSelectedPipelineId] = useState(""); @@ -349,6 +359,8 @@ export default function PipelinesPage() { @@ -121,23 +128,27 @@ export function MessageComposer({ onChange={handleChange} onKeyDown={handleKeyDown} placeholder={ - sessionExpired - ? "Session expired - use a template" - : "Type a message... (Shift+Enter for new line)" + readOnly + ? "Read-only — viewers can browse but not reply" + : sessionExpired + ? "Session expired - use a template" + : "Type a message... (Shift+Enter for new line)" } - disabled={sessionExpired} + disabled={sessionExpired || readOnly} rows={1} + title={readOnly ? "Read-only — your role can't send messages" : undefined} className={cn( "flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50", - sessionExpired && "cursor-not-allowed opacity-50" + (sessionExpired || readOnly) && "cursor-not-allowed opacity-50" )} /> diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index d550745..316d4bd 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -17,6 +17,7 @@ import { Settings, LogOut, User, + UsersRound, X, } from "lucide-react"; import { @@ -63,10 +64,17 @@ interface SidebarProps { onClose?: () => void; } +// Same flag key the Members tab and Settings page use. Flip per +// profile via Supabase Studio to dogfood the multi-user surface. +const ACCOUNT_SHARING_FLAG = "account_sharing"; + export function Sidebar({ open = false, onClose }: SidebarProps) { const pathname = usePathname(); - const { profile, signOut } = useAuth(); + const { profile, account, accountRole, signOut } = useAuth(); const totalUnread = useTotalUnread(); + const accountSharingEnabled = !!profile?.beta_features?.includes( + ACCOUNT_SHARING_FLAG, + ); // Close the drawer when route changes — users opened it to navigate, // so once they pick a destination the drawer should get out of the way. @@ -216,6 +224,23 @@ export function Sidebar({ open = false, onClose }: SidebarProps) { {/* User section */}
+ {/* Account name display — only surfaced when the user is + opted into the account_sharing beta flag. For solo + users (the default) the account is named after them, + so showing it here would just duplicate the user name + below. Once the flag is on the user is at minimum + aware of which shared account they're acting in. */} + {accountSharingEnabled && account?.name ? ( +
+ + {account.name} + {accountRole && accountRole !== "owner" ? ( + + {accountRole} + + ) : null} +
+ ) : null}