Complete recipient search and delivery evidence

This commit is contained in:
2026-07-30 05:22:09 +02:00
parent c769be39da
commit 4a120e8009
6 changed files with 214 additions and 3 deletions
@@ -24,7 +24,7 @@ import { DismissibleAlert } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { SegmentedControl } from "@govoplan/core-webui";
import { TableActionGroup } from "@govoplan/core-webui";
import { DataGrid, DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "@govoplan/core-webui";
import { DataGrid, DataGridEmptyAction, DataGridRowActions, type DataGridColumn, type DataGridQueryState } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
@@ -124,6 +124,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
const [addressSources, setAddressSources] = useState<CampaignRecipientAddressSource[]>([]);
const [recipientProfilesPage, setRecipientProfilesPage] = useState(1);
const [recipientProfilesPageSize, setRecipientProfilesPageSize] = useState(10);
const [recipientProfilesQuery, setRecipientProfilesQuery] = useState<DataGridQueryState>({ sort: null, filters: {} });
const [recipientAddressEditorIndex, setRecipientAddressEditorIndex] = useState<number | null>(null);
const [postboxTargetEditorIndex, setPostboxTargetEditorIndex] = useState<number | null>(null);
const [postboxCatalog, setPostboxCatalog] = useState<CampaignPostboxCatalog>({
@@ -526,6 +527,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
fieldDefinitions,
individualAttachmentBasePaths,
zipConfig,
addressFilter: recipientProfilesQuery.filters.recipients ?? "",
translateText,
openAddressEditor: setRecipientAddressEditorIndex,
openPostboxTargetEditor: setPostboxTargetEditorIndex,
@@ -549,7 +551,8 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
setRecipientProfilesPageSize(pageSize);
setRecipientProfilesPage(1);
}
}} />
}}
onQueryChange={setRecipientProfilesQuery} />
</div>
}
@@ -1735,6 +1738,7 @@ type RecipientProfileColumnContext = {
fieldDefinitions: ReturnType<typeof getDraftFields>;
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
zipConfig: AttachmentZipCollection;
addressFilter: string;
translateText: (value: string) => string;
openAddressEditor: (index: number) => void;
openPostboxTargetEditor: (index: number) => void;
@@ -1746,7 +1750,7 @@ type RecipientProfileColumnContext = {
removeEntry: (index: number) => void;
};
function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, addressFilter, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
return [
{
id: "number",
@@ -1770,6 +1774,7 @@ function recipientProfileColumns({ settings, campaignId, draft, locked, filesMod
filterable: true,
render: (entry, index) => {
const summary = recipientAddressSummary(entry, translateText);
const hiddenMatch = hiddenRecipientAddressMatch(entry, addressFilter);
const content = (
<>
<span className="recipient-address-editor-main">
@@ -1777,6 +1782,11 @@ function recipientProfileColumns({ settings, campaignId, draft, locked, filesMod
{summary.badges.map((badge) =>
<span className="recipient-extra-bubble" key={badge}>{badge}</span>
)}
{hiddenMatch &&
<span className="recipient-hidden-address-match">
Matched in {hiddenMatch.label}: {hiddenMatch.address}
</span>
}
</span>
{!locked && <Pencil aria-hidden="true" />}
</>
@@ -1936,6 +1946,32 @@ function recipientAddressFilterValue(entry: Record<string, unknown>): string {
join(", ");
}
function hiddenRecipientAddressMatch(
entry: Record<string, unknown>,
filterValue: string
): {label: string;address: string;} | null {
const query = filterValue.trim().toLowerCase();
if (!query) return null;
const primary = getEntryAddresses(entry, "to")[0];
if (primary && formatMailboxAddress(primary).toLowerCase().includes(query)) return null;
const labels: Record<AddressFieldKey, string> = {
from: "From",
reply_to: "Reply-To",
to: "To",
cc: "CC",
bcc: "BCC"
};
for (const column of recipientAddressOverlayColumns) {
const addresses = getEntryAddresses(entry, column.key);
const hiddenAddresses = column.key === "to" ? addresses.slice(1) : addresses;
const match = hiddenAddresses.find((address) => formatMailboxAddress(address).toLowerCase().includes(query));
if (match) return { label: labels[column.key], address: formatMailboxAddress(match) };
}
return null;
}
function addressCountBadge(count: number, label: string): string | null {
if (count <= 0) return null;
return `+${count} ${label}`;