import { useEffect, useMemo, useState } from "react"; import type { ApiSettings, AuthInfo, CampaignListItem } from "../../../types"; import { ArrowRightLeft, Check, KeyRound, Trash2, XCircle } from "lucide-react"; import { fetchResourceAccessExplanation, getCampaignShares, campaignShareTargetProvider, revokeCampaignShare, upsertCampaignShare, type CampaignShare, type ResourceAccessExplanationResponse } from "../../../api/campaigns"; import { Button, decideOwnershipTransfer, listOwnershipTransfers, ReferenceSelect, ResourceAccessExplanation, requestResourceOwnership, startOwnershipTransfer, type OwnershipTransfer, type ReferenceOption, type ReferenceOptionProvider } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge, TableActionGroup, hasScope, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; type TargetType = "user" | "group"; export default function CampaignAccessCard({ settings, auth, campaign, onChanged, onError }: {settings: ApiSettings;auth: AuthInfo;campaign: CampaignListItem;onChanged: () => Promise;onError: (message: string) => void;}) { const [available, setAvailable] = useState(null); const [targets, setTargets] = useState<{ users: readonly ReferenceOption[]; groups: readonly ReferenceOption[]; }>({ users: [], groups: [] }); const [shares, setShares] = useState([]); const [ownerOpen, setOwnerOpen] = useState(false); const [requestOpen, setRequestOpen] = 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 [ownerReason, setOwnerReason] = useState(""); const [ownerExpiryDays, setOwnerExpiryDays] = useState(7); const [ownerRequestKey, setOwnerRequestKey] = useState(""); const [requestTargetType, setRequestTargetType] = useState("user"); const [requestGroupId, setRequestGroupId] = useState(""); const [requestReason, setRequestReason] = useState(""); const [requestKey, setRequestKey] = useState(""); const [shareType, setShareType] = useState("user"); const [shareTargetId, setShareTargetId] = useState(""); const [sharePermission, setSharePermission] = useState<"read" | "write">("read"); const [accessExplanationOpen, setAccessExplanationOpen] = useState(false); const [accessExplanation, setAccessExplanation] = useState(null); const [accessExplanationLoading, setAccessExplanationLoading] = useState(false); const [ownershipTransfers, setOwnershipTransfers] = useState([]); const [busy, setBusy] = useState(false); const currentOwnerType: TargetType = campaign.owner_group_id ? "group" : "user"; const currentOwnerId = campaign.owner_group_id || campaign.owner_user_id || ""; const actorId = auth.principal?.membership_id || auth.user.id; const actorGroupIds = new Set( auth.principal?.group_ids ?? auth.groups.map((group) => group.id) ); const actorIsCurrentOwner = ( currentOwnerType === "user" ? currentOwnerId === actorId : actorGroupIds.has(currentOwnerId) ); const canProposeOwnership = actorIsCurrentOwner && hasScope( auth, "campaigns:campaign:share" ); const canRequestOwnership = !actorIsCurrentOwner && hasScope( auth, "campaigns:campaign:read" ); const ownerDirty = ownerOpen && ( ownerType !== currentOwnerType || ownerId !== currentOwnerId || Boolean(ownerReason) || ownerExpiryDays !== 7 ); const requestDirty = requestOpen && ( requestTargetType !== "user" || Boolean(requestGroupId) || Boolean(requestReason) ); const shareDirty = shareOpen && (shareType !== "user" || Boolean(shareTargetId) || sharePermission !== "read"); const canExplainResourceAccess = hasScope(auth, "admin:users:read") || hasScope(auth, "admin:roles:read") || hasScope(auth, "access:membership:read") || hasScope(auth, "access:role:read"); const userTargetProvider = useMemo( () => campaignShareTargetProvider(settings, campaign.id, "user"), [campaign.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey] ); const groupTargetProvider = useMemo( () => campaignShareTargetProvider(settings, campaign.id, "group"), [campaign.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey] ); useUnsavedDraftGuard({ dirty: ownerDirty || requestDirty || shareDirty, onSave: saveActiveDialog, onDiscard: () => { closeOwnerDialog(); closeRequestDialog(); closeShareDialog(); } }); async function load() { let nextShares: CampaignShare[] = []; let nextTransfers: OwnershipTransfer[] = []; try { nextTransfers = await listOwnershipTransfers(settings, { resourceType: "campaign", resourceId: campaign.id, limit: 100 }); setOwnershipTransfers(nextTransfers); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } try { nextShares = (await getCampaignShares(settings, campaign.id)) .filter((item) => !item.revoked_at); setShares(nextShares); setAvailable(true); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.startsWith("403 ")) { setAvailable(false); } else { onError(message); } } const userIds = uniqueTargetIds( nextShares, nextTransfers, "user", campaign.owner_user_id ); const groupIds = uniqueTargetIds( nextShares, nextTransfers, "group", campaign.owner_group_id ); const controller = new AbortController(); try { const [users, groups] = await Promise.all([ resolveReferenceOptions(userTargetProvider, userIds, controller.signal), resolveReferenceOptions(groupTargetProvider, groupIds, controller.signal) ]); setTargets({ users, groups }); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } } 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, groupTargetProvider, settings.accessToken, settings.apiBaseUrl, settings.apiKey, userTargetProvider ]); const ownerTargetProvider = ownerType === "user" ? userTargetProvider : groupTargetProvider; const shareTargetProvider = shareType === "user" ? userTargetProvider : groupTargetProvider; const targetMap = useMemo(() => new Map([ ...targets.users.map((item) => [`user:${item.value}`, item] as const), ...targets.groups.map((item) => [`group:${item.value}`, item] as const)] ), [targets]); const ownerLabel = campaign.owner_group_id ? targetMap.get(`group:${campaign.owner_group_id}`)?.label || "i18n:govoplan-campaign.group_owner.55630670" : targetMap.get(`user:${campaign.owner_user_id || ""}`)?.label || "i18n:govoplan-campaign.user_owner.86e3faab"; function closeOwnerDialog() { setOwnerOpen(false); setOwnerType(currentOwnerType); setOwnerId(currentOwnerId); setOwnerReason(""); setOwnerExpiryDays(7); setOwnerRequestKey(""); } function openOwnerDialog() { setOwnerType(currentOwnerType); setOwnerId(""); setOwnerReason(""); setOwnerExpiryDays(7); setOwnerRequestKey(crypto.randomUUID()); setOwnerOpen(true); } function closeRequestDialog() { setRequestOpen(false); setRequestTargetType("user"); setRequestGroupId(""); setRequestReason(""); setRequestKey(""); } function openRequestDialog() { setRequestTargetType("user"); setRequestGroupId(""); setRequestReason(""); setRequestKey(crypto.randomUUID()); setRequestOpen(true); } function closeShareDialog() { setShareOpen(false); setShareType("user"); setShareTargetId(""); setSharePermission("read"); } async function saveActiveDialog(): Promise { if (ownerDirty) return saveOwner(); if (requestDirty) return requestOwnership(); if (shareDirty) return saveShare(); return true; } async function saveOwner(): Promise { if (!ownerId || ownerId === currentOwnerId && ownerType === currentOwnerType) { return false; } setBusy(true); try { await startOwnershipTransfer(settings, { resource: { module_id: "campaigns", resource_type: "campaign", resource_id: campaign.id }, target_owner: { type: ownerType, id: ownerId }, idempotency_key: ownerRequestKey || crypto.randomUUID(), reason: ownerReason.trim() || null, expiry_days: ownerExpiryDays }); closeOwnerDialog(); await load(); return true; } catch (err) {onError(err instanceof Error ? err.message : String(err));return false;} finally {setBusy(false);} } async function requestOwnership(): Promise { const targetId = requestTargetType === "user" ? actorId : requestGroupId; if (!targetId) return false; setBusy(true); try { await requestResourceOwnership(settings, { resource: { module_id: "campaigns", resource_type: "campaign", resource_id: campaign.id }, target_owner: { type: requestTargetType, id: targetId }, idempotency_key: requestKey || crypto.randomUUID(), reason: requestReason.trim() || null, expiry_days: 7 }); closeRequestDialog(); await load(); return true; } catch (err) { onError(err instanceof Error ? err.message : String(err)); return false; } finally { setBusy(false); } } async function performOwnershipAction( transfer: OwnershipTransfer, action: "owner-approval" | "acceptance" | "decline" | "cancel" ) { setBusy(true); try { await decideOwnershipTransfer(settings, transfer.id, action); await onChanged(); await load(); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function saveShare(): Promise { if (!shareTargetId) return false; setBusy(true); try { await upsertCampaignShare(settings, campaign.id, { target_type: shareType, target_id: shareTargetId, permission: sharePermission }); setShareOpen(false); setShareTargetId(""); await load(); return true; } catch (err) {onError(err instanceof Error ? err.message : String(err));return false;} 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);} } async function openAccessExplanation() { if (!auth.user?.id) return; setAccessExplanationOpen(true); setAccessExplanation(null); setAccessExplanationLoading(true); try { setAccessExplanation(await fetchResourceAccessExplanation(settings, { userId: auth.user.id, resourceType: "campaign", resourceId: campaign.id, action: "campaigns:campaign:read", tenantId: (auth.active_tenant ?? auth.tenant)?.id })); } catch (err) { onError(err instanceof Error ? err.message : String(err)); setAccessExplanationOpen(false); } finally { setAccessExplanationLoading(false); } } const columns = useMemo[]>(() => [ { id: "target", header: "i18n:govoplan-campaign.shared_with.6203f449", width: "minmax(220px, 1fr)", minWidth: 190, maxWidth: 560, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => targetMap.get(`${row.target_type}:${row.target_id}`)?.label || row.target_id, render: (row) => { const target = targetMap.get(`${row.target_type}:${row.target_id}`); return
{target?.label || row.target_id}
{row.target_type}{target?.description ? i18nMessage("i18n:govoplan-campaign.value.48afe802", { value0: target.description }) : ""}
; } }, { id: "permission", header: "i18n:govoplan-campaign.access.2f81a22d", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.permission, render: (row) => }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 72, resizable: false, sticky: "end", align: "right", render: (row) =>