Auto-refresh submissions, fix pagination, add send email shortcut
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m16s
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m16s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user