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 { import {
CopyOutlined, CopyOutlined,
DeleteOutlined, DeleteOutlined,
MailOutlined,
ReloadOutlined, ReloadOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import {
@@ -46,6 +47,8 @@ export default function AdminSubmissionsCard({
onReload, onReload,
copyText, copyText,
onCopy, onCopy,
mailtoHref,
onOpenMailto,
selectedRowKeys, selectedRowKeys,
onSelectedRowKeysChange, onSelectedRowKeysChange,
pagedSubmissions, pagedSubmissions,
@@ -67,6 +70,8 @@ export default function AdminSubmissionsCard({
onReload: () => void; onReload: () => void;
copyText: string; copyText: string;
onCopy: (text: string, didCopy: boolean) => void; onCopy: (text: string, didCopy: boolean) => void;
mailtoHref: string;
onOpenMailto: () => void;
selectedRowKeys: Array<string | number>; selectedRowKeys: Array<string | number>;
onSelectedRowKeysChange: (keys: Array<string | number>) => void; onSelectedRowKeysChange: (keys: Array<string | number>) => void;
pagedSubmissions: any[]; pagedSubmissions: any[];
@@ -175,6 +180,21 @@ export default function AdminSubmissionsCard({
</Button> </Button>
</CopyToClipboard> </CopyToClipboard>
</Tooltip> </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"> <Tooltip title="Reload latest order details">
<Button icon={<ReloadOutlined />} onClick={onReload} /> <Button icon={<ReloadOutlined />} onClick={onReload} />
</Tooltip> </Tooltip>
+96 -21
View File
@@ -49,6 +49,10 @@ function getEstimatedTotalNumber(rawValue: unknown): number | null {
return null; return null;
} }
function getCleanSubmissionText(submission: Submission): string {
return stripPriceDecorations(String(submission.formatted_string || "")).trim();
}
export default function AdminView({ orderId }: { orderId: string }) { export default function AdminView({ orderId }: { orderId: string }) {
const [deletingId, setDeletingId] = useState<string | number | null>(null); const [deletingId, setDeletingId] = useState<string | number | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
@@ -105,9 +109,6 @@ export default function AdminView({ orderId }: { orderId: string }) {
run: loadAdmin, run: loadAdmin,
} = useApiRequest(loadAdminData, { } = useApiRequest(loadAdminData, {
deps: [loadAdminData], deps: [loadAdminData],
onSuccess: () => {
setSelectedRowKeys([]);
},
onError: (requestError) => { onError: (requestError) => {
if (shouldRedirectToParticipant(requestError)) { if (shouldRedirectToParticipant(requestError)) {
replaceRoute(`/order/${orderId}`); replaceRoute(`/order/${orderId}`);
@@ -229,6 +230,39 @@ export default function AdminView({ orderId }: { orderId: string }) {
message.success("Menu configuration updated"); 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(() => { useEffect(() => {
if (!data) { if (!data) {
return; return;
@@ -276,32 +310,42 @@ export default function AdminView({ orderId }: { orderId: string }) {
useEffect(() => { useEffect(() => {
const visibleIds = new Set(filteredSubmissions.map((submission: any) => submission.id)); const visibleIds = new Set(filteredSubmissions.map((submission: any) => submission.id));
setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id))); setSelectedRowKeys((previous) => previous.filter((id) => visibleIds.has(id)));
setSubmissionsPage(1); }, [filteredSubmissions]);
}, [searchQuery, selectedSubmissionState, filteredSubmissions]);
if (pageError || loading) { useEffect(() => {
return ( setSubmissionsPage(1);
<AsyncContent }, [searchQuery, selectedSubmissionState]);
loading={loading}
error={pageError}
onRetry={loadAdmin}
errorTitle="Admin page unavailable"
loadingRows={4}
loadingSections={2}
/>
);
}
const selected = filteredSubmissions.filter((submission: any) => const selected = filteredSubmissions.filter((submission: any) =>
selectedRowKeys.includes(submission.id), selectedRowKeys.includes(submission.id),
); );
const rowsToCopy = selected.length > 0 ? selected : filteredSubmissions; const rowsToCopy = selected.length > 0 ? selected : filteredSubmissions;
const copyText = rowsToCopy const copyText = rowsToCopy
.map((submission: any) => .map((submission: Submission) => getCleanSubmissionText(submission))
stripPriceDecorations(String(submission.formatted_string || "")).trim(),
)
.filter(Boolean) .filter(Boolean)
.join("\n"); .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 totalEstimatedValue = rowsToCopy.reduce((sum: number, submission: any) => {
const value = getEstimatedTotalNumber(submission.estimated_total); const value = getEstimatedTotalNumber(submission.estimated_total);
@@ -318,6 +362,19 @@ export default function AdminView({ orderId }: { orderId: string }) {
submissionsPage * SUBMISSIONS_PAGE_SIZE, 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) => { const copySubmissionList = (_text: string, didCopy: boolean) => {
if (!copyText) { if (!copyText) {
message.warning("Nothing to copy."); 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 ( return (
<Space direction="vertical" style={{ width: "100%" }} size={16}> <Space direction="vertical" style={{ width: "100%" }} size={16}>
<AdminParticipantShareAlert orderId={orderId} /> <AdminParticipantShareAlert orderId={orderId} />
@@ -349,9 +422,11 @@ export default function AdminView({ orderId }: { orderId: string }) {
selectedSubmissionState={selectedSubmissionState} selectedSubmissionState={selectedSubmissionState}
onSearchQueryChange={setSearchQuery} onSearchQueryChange={setSearchQuery}
onSelectedSubmissionStateChange={setSelectedSubmissionState} onSelectedSubmissionStateChange={setSelectedSubmissionState}
onReload={loadAdmin} onReload={refreshSubmissions}
copyText={copyText} copyText={copyText}
onCopy={copySubmissionList} onCopy={copySubmissionList}
mailtoHref={mailtoHref}
onOpenMailto={openSubmissionEmailDraft}
selectedRowKeys={selectedRowKeys} selectedRowKeys={selectedRowKeys}
onSelectedRowKeysChange={setSelectedRowKeys} onSelectedRowKeysChange={setSelectedRowKeys}
pagedSubmissions={pagedSubmissions} pagedSubmissions={pagedSubmissions}