import { useEffect, useMemo, useState } from "react"; import type { ApiSettings, CampaignListItem } from "../../../types"; import { getCampaignShares, getCampaignShareTargets, revokeCampaignShare, updateCampaignOwner, upsertCampaignShare, type CampaignShare, type CampaignShareTargets } from "../../../api/campaigns"; import Button from "../../../components/Button"; import Card from "../../../components/Card"; import ConfirmDialog from "../../../components/ConfirmDialog"; import DataGrid, { type DataGridColumn } from "../../../components/table/DataGrid"; import Dialog from "../../../components/Dialog"; import FormField from "../../../components/FormField"; import StatusBadge from "../../../components/StatusBadge"; type TargetType = "user" | "group"; export default function CampaignAccessCard({ settings, campaign, onChanged, onError }: { settings: ApiSettings; campaign: CampaignListItem; onChanged: () => Promise; onError: (message: string) => void; }) { const [available, setAvailable] = useState(null); const [targets, setTargets] = useState({ users: [], groups: [] }); const [shares, setShares] = useState([]); const [ownerOpen, setOwnerOpen] = useState(false); const [shareOpen, setShareOpen] = useState(false); const [revokeTarget, setRevokeTarget] = useState(null); const [ownerType, setOwnerType] = useState(campaign.owner_group_id ? "group" : "user"); const [ownerId, setOwnerId] = useState(campaign.owner_group_id || campaign.owner_user_id || ""); const [shareType, setShareType] = useState("user"); const [shareTargetId, setShareTargetId] = useState(""); const [sharePermission, setSharePermission] = useState<"read" | "write">("read"); const [busy, setBusy] = useState(false); async function load() { try { const [nextTargets, nextShares] = await Promise.all([ getCampaignShareTargets(settings, campaign.id), getCampaignShares(settings, campaign.id) ]); setTargets(nextTargets); setShares(nextShares.filter((item) => !item.revoked_at)); setAvailable(true); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.startsWith("403 ")) setAvailable(false); else onError(message); } } useEffect(() => { setOwnerType(campaign.owner_group_id ? "group" : "user"); setOwnerId(campaign.owner_group_id || campaign.owner_user_id || ""); void load(); }, [campaign.id, campaign.owner_group_id, campaign.owner_user_id, settings.accessToken, settings.apiBaseUrl]); const targetOptions = ownerType === "user" ? targets.users : targets.groups; const shareTargetOptions = shareType === "user" ? targets.users : targets.groups; const targetMap = useMemo(() => new Map([ ...targets.users.map((item) => [`user:${item.id}`, item] as const), ...targets.groups.map((item) => [`group:${item.id}`, item] as const) ]), [targets]); const ownerLabel = campaign.owner_group_id ? targetMap.get(`group:${campaign.owner_group_id}`)?.name || "Group owner" : targetMap.get(`user:${campaign.owner_user_id || ""}`)?.name || "User owner"; async function saveOwner() { if (!ownerId) return; setBusy(true); try { await updateCampaignOwner(settings, campaign.id, ownerType === "user" ? { owner_user_id: ownerId, owner_group_id: null } : { owner_user_id: null, owner_group_id: ownerId }); setOwnerOpen(false); await onChanged(); await load(); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function saveShare() { if (!shareTargetId) return; setBusy(true); try { await upsertCampaignShare(settings, campaign.id, { target_type: shareType, target_id: shareTargetId, permission: sharePermission }); setShareOpen(false); setShareTargetId(""); await load(); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function revoke() { if (!revokeTarget) return; setBusy(true); try { await revokeCampaignShare(settings, campaign.id, revokeTarget.id); setRevokeTarget(null); await load(); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } const columns = useMemo[]>(() => [ { id: "target", header: "Shared with", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => targetMap.get(`${row.target_type}:${row.target_id}`)?.name || row.target_id, render: (row) => { const target = targetMap.get(`${row.target_type}:${row.target_id}`); return
{target?.name || row.target_id}
{row.target_type}{target?.secondary ? ` · ${target.secondary}` : ""}
; } }, { id: "permission", header: "Access", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.permission, render: (row) => }, { id: "actions", header: "Actions", width: 110, resizable: false, sticky: "end", align: "right", render: (row) => } ], [targetMap]); if (available === false) return null; return ( <> }>

Owner: {ownerLabel}

Permissions define what a role may do; ownership and explicit shares determine which campaigns that permission applies to.

row.id} emptyText={available === null ? "Loading campaign access…" : "This campaign has no explicit shares."} />
!busy && setOwnerOpen(false)} footer={<>}> !busy && setShareOpen(false)} footer={<>}> setRevokeTarget(null)} onConfirm={() => void revoke()} /> ); }