Auto-refresh submissions, fix pagination, add send email shortcut
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m16s

This commit is contained in:
Simon Gruber
2026-04-01 08:37:13 +02:00
parent 0b437a2314
commit 696e881361
2 changed files with 116 additions and 21 deletions
@@ -3,6 +3,7 @@ import { CopyToClipboard } from "react-copy-to-clipboard";
import {
CopyOutlined,
DeleteOutlined,
MailOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import {
@@ -46,6 +47,8 @@ export default function AdminSubmissionsCard({
onReload,
copyText,
onCopy,
mailtoHref,
onOpenMailto,
selectedRowKeys,
onSelectedRowKeysChange,
pagedSubmissions,
@@ -67,6 +70,8 @@ export default function AdminSubmissionsCard({
onReload: () => void;
copyText: string;
onCopy: (text: string, didCopy: boolean) => void;
mailtoHref: string;
onOpenMailto: () => void;
selectedRowKeys: Array<string | number>;
onSelectedRowKeysChange: (keys: Array<string | number>) => void;
pagedSubmissions: any[];
@@ -175,6 +180,21 @@ export default function AdminSubmissionsCard({
</Button>
</CopyToClipboard>
</Tooltip>
<Tooltip
title={
selectedRowKeys.length > 0
? "Create draft email with selected submissions"
: "Create draft email with submissions"
}
>
<Button
icon={<MailOutlined />}
href={mailtoHref || undefined}
onClick={onOpenMailto}
>
{selectedRowKeys.length > 0 ? "Email (selected)" : "Email"}
</Button>
</Tooltip>
<Tooltip title="Reload latest order details">
<Button icon={<ReloadOutlined />} onClick={onReload} />
</Tooltip>
+96 -21
View File
@@ -49,6 +49,10 @@ function getEstimatedTotalNumber(rawValue: unknown): number | null {
return null;
}
function getCleanSubmissionText(submission: Submission): string {
return stripPriceDecorations(String(submission.formatted_string || "")).trim();
}
export default function AdminView({ orderId }: { orderId: string }) {
const [deletingId, setDeletingId] = useState<string | number | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
@@ -105,9 +109,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
run: loadAdmin,
} = useApiRequest(loadAdminData, {
deps: [loadAdminData],
onSuccess: () => {
setSelectedRowKeys([]);
},
onError: (requestError) => {
if (shouldRedirectToParticipant(requestError)) {
replaceRoute(`/order/${orderId}`);
@@ -229,6 +230,39 @@ export default function AdminView({ orderId }: { orderId: string }) {
message.success("Menu configuration updated");
};
const refreshSubmissions = useCallback(async () => {
try {
const adminPayload = await apiService.orders.getAdminView(orderId);
const typedPayload = adminPayload as GetAdminViewResponse;
const nextSubmissions = Array.isArray(typedPayload?.submissions)
? typedPayload.submissions
: [];
setData((previous: AdminViewData | null) => {
if (!previous) {
return previous;
}
return {
...previous,
submissions: nextSubmissions,
};
});
} catch {
// Keep auto-refresh silent to avoid repeated toast noise.
}
}, [orderId, setData]);
useEffect(() => {
const intervalId = window.setInterval(() => {
void refreshSubmissions();
}, 10_000);
return () => {
window.clearInterval(intervalId);
};
}, [refreshSubmissions]);
useEffect(() => {
if (!data) {
return;
@@ -276,32 +310,42 @@ export default function AdminView({ orderId }: { orderId: string }) {
useEffect(() => {
const visibleIds = new Set(filteredSubmissions.map((submission: any) => submission.id));
setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id)));
setSubmissionsPage(1);
}, [searchQuery, selectedSubmissionState, filteredSubmissions]);
}, [filteredSubmissions]);
if (pageError || loading) {
return (
<AsyncContent
loading={loading}
error={pageError}
onRetry={loadAdmin}
errorTitle="Admin page unavailable"
loadingRows={4}
loadingSections={2}
/>
);
}
useEffect(() => {
setSubmissionsPage(1);
}, [searchQuery, selectedSubmissionState]);
const selected = filteredSubmissions.filter((submission: any) =>
selectedRowKeys.includes(submission.id),
);
const rowsToCopy = selected.length > 0 ? selected : filteredSubmissions;
const copyText = rowsToCopy
.map((submission: any) =>
stripPriceDecorations(String(submission.formatted_string || "")).trim(),
)
.map((submission: Submission) => getCleanSubmissionText(submission))
.filter(Boolean)
.join("\n");
const ccRecipients = Array.from(
new Set(
rowsToCopy
.map((submission: Submission) => String(submission.email || "").trim())
.filter(Boolean),
),
);
const emailBody = rowsToCopy
.map((submission: Submission) => getCleanSubmissionText(submission))
.filter(Boolean)
.map((line) => `- ${line}`)
.join("\n");
let mailtoHref = "";
if (emailBody) {
const queryParts: string[] = [];
if (ccRecipients.length > 0) {
queryParts.push(`cc=${encodeURIComponent(ccRecipients.join(","))}`);
}
queryParts.push(`subject=${encodeURIComponent(data?.title || "Lunch order submissions")}`);
queryParts.push(`body=${encodeURIComponent(`${emailBody}\n`)}`);
mailtoHref = `mailto:?${queryParts.join("&")}`;
}
const totalEstimatedValue = rowsToCopy.reduce((sum: number, submission: any) => {
const value = getEstimatedTotalNumber(submission.estimated_total);
@@ -318,6 +362,19 @@ export default function AdminView({ orderId }: { orderId: string }) {
submissionsPage * SUBMISSIONS_PAGE_SIZE,
);
if (pageError || loading) {
return (
<AsyncContent
loading={loading}
error={pageError}
onRetry={loadAdmin}
errorTitle="Admin page unavailable"
loadingRows={4}
loadingSections={2}
/>
);
}
const copySubmissionList = (_text: string, didCopy: boolean) => {
if (!copyText) {
message.warning("Nothing to copy.");
@@ -334,6 +391,22 @@ export default function AdminView({ orderId }: { orderId: string }) {
);
};
const openSubmissionEmailDraft = () => {
if (!copyText) {
message.warning("Nothing to email.");
return;
}
if (!mailtoHref) {
message.error("Could not prepare email draft.");
return;
}
message.success(
`Opened draft for ${rowsToCopy.length} ${rowsToCopy.length === 1 ? "entry" : "entries"}`,
);
};
return (
<Space direction="vertical" style={{ width: "100%" }} size={16}>
<AdminParticipantShareAlert orderId={orderId} />
@@ -349,9 +422,11 @@ export default function AdminView({ orderId }: { orderId: string }) {
selectedSubmissionState={selectedSubmissionState}
onSearchQueryChange={setSearchQuery}
onSelectedSubmissionStateChange={setSelectedSubmissionState}
onReload={loadAdmin}
onReload={refreshSubmissions}
copyText={copyText}
onCopy={copySubmissionList}
mailtoHref={mailtoHref}
onOpenMailto={openSubmissionEmailDraft}
selectedRowKeys={selectedRowKeys}
onSelectedRowKeysChange={setSelectedRowKeys}
pagedSubmissions={pagedSubmissions}