refactor(campaign): split review and recipient boundaries

This commit is contained in:
2026-07-30 01:01:26 +02:00
parent 23b9a531d5
commit 961d5d1130
12 changed files with 1860 additions and 1179 deletions

View File

@@ -0,0 +1,191 @@
import { useState } from "react";
import { Link2, X } from "lucide-react";
import { Button, Dialog, i18nMessage } from "@govoplan/core-webui";
import type {
CampaignAttachmentPreviewFile,
CampaignAttachmentPreviewResponse
} from "../../../api/campaigns";
import {
attachmentPreviewLinkableFiles,
attachmentPreviewMatchedFiles
} from "../utils/attachmentPreview";
import { WorkflowFact } from "./WorkflowNavigation";
export default function AttachmentLinkingPreview({
preview,
loading,
error,
linking,
disabled,
onRefresh,
onLink
}: {
preview: CampaignAttachmentPreviewResponse | null;
loading: boolean;
error: string;
linking: boolean;
disabled: boolean;
onRefresh: () => void;
onLink: () => void;
}) {
const [detailsOpen, setDetailsOpen] = useState(false);
const matchedFiles = attachmentPreviewMatchedFiles(preview);
const linkableFiles = attachmentPreviewLinkableFiles(preview);
const linkedCount =
preview?.linked_file_count ??
matchedFiles.filter((file) => file.linked_to_campaign !== false).length;
const matchedCount = preview?.matched_file_count ?? matchedFiles.length;
const unlinkedCount = preview?.unlinked_file_count ?? linkableFiles.length;
return (
<>
<div className="review-flow-data-section attachment-linking-preview">
<div className="attachment-linking-header">
<div>
<h3>i18n:govoplan-campaign.attachment_file_links.0be74fd1</h3>
<p className="muted small-note">
i18n:govoplan-campaign.preview_includes_already_linked_files_and_access.dfef92af
</p>
</div>
<div className="button-row compact-actions">
<Button onClick={onRefresh} disabled={loading || linking}>
{loading
? "i18n:govoplan-campaign.refreshing.505dddc9"
: "i18n:govoplan-campaign.refresh.56e3badc"}
</Button>
<Button
variant="primary"
onClick={onLink}
disabled={disabled || loading || linking || unlinkedCount === 0}
>
{linking
? "i18n:govoplan-campaign.linking.a5f54e0f"
: i18nMessage(
unlinkedCount === 1
? "i18n:govoplan-campaign.link_value_file.4d4ce740"
: "i18n:govoplan-campaign.link_value_files.88b7e6a7",
{ value0: unlinkedCount }
)}
</Button>
</div>
</div>
<div className="review-flow-fact-grid">
<WorkflowFact
label="i18n:govoplan-campaign.matched.1bf3ec5b"
value={
!loading && matchedFiles.length > 0 ? (
<button
type="button"
className="review-flow-fact-detail-button"
aria-haspopup="dialog"
aria-expanded={detailsOpen}
aria-label={i18nMessage(
"i18n:govoplan-campaign.view_all_value_matched_attachment_file_links.81e53951",
{ value0: matchedFiles.length }
)}
onClick={() => setDetailsOpen(true)}
>
{matchedCount}
</button>
) : loading ? (
"..."
) : (
matchedCount || "—"
)
}
/>
<WorkflowFact
label="i18n:govoplan-campaign.linked.a089f600"
value={loading ? "..." : linkedCount || "—"}
/>
<WorkflowFact
label="i18n:govoplan-campaign.need_link.fa4ab530"
value={loading ? "..." : unlinkedCount || "—"}
/>
<WorkflowFact
label="i18n:govoplan-campaign.shared_source.7d4a1bf2"
value={loading ? "..." : preview?.shared_file_count ?? "—"}
/>
</div>
{error && <p className="review-flow-inline-note is-danger">{error}</p>}
{!error && unlinkedCount > 0 && (
<p className="review-flow-inline-note is-stale">
i18n:govoplan-campaign.unlinked_candidate_files_are_not_yet_part_of_the.b8fd5998
</p>
)}
{!error && !loading && matchedFiles.length === 0 && (
<p className="muted small-note">
i18n:govoplan-campaign.no_managed_files_matched_the_current_attachment_.dba99f5c
</p>
)}
{matchedFiles.length > 0 && <AttachmentLinkingFileList files={matchedFiles} compact />}
</div>
<Dialog
open={detailsOpen}
title={i18nMessage(
"i18n:govoplan-campaign.attachment_file_links_value.ce230e30",
{ value0: matchedFiles.length }
)}
className="attachment-linking-detail-modal"
bodyClassName="attachment-linking-detail-body"
onClose={() => setDetailsOpen(false)}
footer={
<Button variant="primary" onClick={() => setDetailsOpen(false)}>
i18n:govoplan-campaign.close.bbfa773e
</Button>
}
>
<p className="muted small-note">
i18n:govoplan-campaign.all_unique_managed_files_matched_by_the_current_.0214c3e5
</p>
<AttachmentLinkingFileList files={matchedFiles} />
</Dialog>
</>
);
}
function AttachmentLinkingFileList({
files,
compact = false
}: {
files: CampaignAttachmentPreviewFile[];
compact?: boolean;
}) {
return (
<ul
className={`attachment-linking-file-list${compact ? " is-compact" : " is-detail"}`}
tabIndex={0}
aria-label={i18nMessage(
files.length === 1
? "i18n:govoplan-campaign.value_matched_attachment_file_link.30a84824"
: "i18n:govoplan-campaign.value_matched_attachment_file_links.ce509a3a",
{ value0: files.length }
)}
>
{files.map((file, index) => {
const linked = file.linked_to_campaign !== false;
const Icon = linked ? Link2 : X;
return (
<li
key={`${file.id || file.display_path || file.filename}:${index}`}
data-linked={linked ? "true" : "false"}
>
<Icon size={15} strokeWidth={2.2} aria-hidden="true" />
<span>
<strong>{file.filename || file.display_path}</strong>
<small>
{linked
? "i18n:govoplan-campaign.linked_already_part_of_the_campaign_file_snapsho.d037a6bc"
: "i18n:govoplan-campaign.unlinked_candidate_match_potentially_missing_unt.2bae9433"}
</small>
{file.display_path && file.display_path !== file.filename && (
<small className="muted">{file.display_path}</small>
)}
</span>
</li>
);
})}
</ul>
);
}