payment ux refinement
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m37s

This commit is contained in:
Simon Gruber committed 2026-10-21 08:07:44 +02:00
1 parent e53f814451
commit 616ac5583f
2 files changed
+81 -84

No files matched your search

@@ -1,5 +1,6 @@
import React from "react";
import { Checkbox, Result, Space, Tooltip, Typography } from "antd";
import { Button, Flex, Result, Space, Tag, Tooltip, Typography, theme } from "antd";
import { CheckCircleFilled, CheckOutlined, WalletOutlined } from "@ant-design/icons";
import type { Submission } from "../../lib/types";
const { Text } = Typography;
@@ -22,12 +23,17 @@ export default function ParticipantSubmissionSummary({
savedOrderString,
savedEstimatedTotal,
isClosed,
onHowToPay,
}: {
submission: Submission | null;
savedOrderString: string;
savedEstimatedTotal: string | null;
isClosed: boolean;
/** Shown as a "How to pay" button when payment details exist. */
onHowToPay?: () => void;
}) {
const { token } = theme.useToken();
if (!submission) {
return (
<Result
@@ -42,44 +48,55 @@ export default function ParticipantSubmissionSummary({
);
}
const statuses = [
{ label: "Accepted", done: submission.accepted },
{ label: "Paid", done: submission.paid },
{ label: "Received", done: !!submission.handed_out },
];
return (
<Result
status="success"
title="Order submitted"
subTitle={
<Space direction="vertical">
<Tooltip title="Your submitted order">
<Text code copyable>
{savedOrderString}
</Text>
</Tooltip>
<Flex vertical gap={8}>
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
<Text strong>
<CheckCircleFilled style={{ color: token.colorSuccess }} /> Order submitted
</Text>
<Flex gap={4} wrap="wrap">
{statuses.map(({ label, done }) => (
<Tag
key={label}
bordered={false}
color={done ? "success" : "default"}
icon={done ? <CheckOutlined /> : undefined}
style={{ marginInlineEnd: 0 }}
>
{label}
</Tag>
))}
</Flex>
</Flex>
<Tooltip title="Your submitted order">
<Text code copyable>
{savedOrderString}
</Text>
</Tooltip>
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
<Space size={4} direction="vertical">
{savedEstimatedTotal && (
<Tooltip title="Rough estimate based on configured item prices. Final amount may differ.">
<Text type="secondary">Estimated total: {savedEstimatedTotal}</Text>
</Tooltip>
)}
{!isClosed && <Text>Review or adjust at any time while the order is open</Text>}
</Space>
}
extra={[
<Space key="submission-status" direction="vertical" size={4}>
<Text strong>Submission status</Text>
<Space size={20} wrap>
<Checkbox checked={submission.accepted} style={{ pointerEvents: "none" }}>
Accepted
</Checkbox>
<Checkbox checked={submission.paid} style={{ pointerEvents: "none" }}>
Paid
</Checkbox>
<Checkbox checked={!!submission.handed_out} style={{ pointerEvents: "none" }}>
Received
</Checkbox>
</Space>
<Text type="secondary" style={{ fontSize: 12 }}>
Updated {formatTimestamp(submission.updated_at)}
{!isClosed && "You can adjust your order while it is open. "}Updated{" "}
{formatTimestamp(submission.updated_at)}
</Text>
</Space>,
]}
/>
</Space>
{onHowToPay && !submission.paid && (
<Button size="small" type="primary" icon={<WalletOutlined />} onClick={onHowToPay}>
How to pay
</Button>
)}
</Flex>
</Flex>
);
}
+32 -52
View File
@@ -8,7 +8,6 @@ import {
Popconfirm,
Row,
Space,
Steps,
Tooltip,
Typography,
message,
@@ -20,7 +19,7 @@ import { navigateTo } from "../lib/routing";
import { apiService } from "../lib/services";
import LoadingSkeleton from "../components/utils/LoadingSkeleton";
import ReadOnlyOrderOverviewCard from "../components/orders/ReadOnlyOrderOverviewCard";
import PaymentDetailsCard from "../components/orders/PaymentDetailsCard";
import { PaymentMethodsAccordion } from "../components/orders/PaymentDetailsCard";
import { normalizePaymentDetails, type PaymentContext } from "../lib/paymentDetails";
import {
buildFormattedOrderString,
@@ -42,7 +41,7 @@ import ParticipantDraftSummaryAlert from "../components/views/ParticipantDraftSu
const { Text, Link } = Typography;
const PAYMENT_CARD_ID = "order-payment";
const OVERVIEW_CARD_ID = "order-overview";
type SubmissionValues = {
choices: NormalizedOrderSelections;
@@ -91,6 +90,7 @@ type ParticipantLoadData = {
export default function ParticipantView({ orderId }: { orderId: string }) {
const [form] = Form.useForm<FormModel>();
const watchedValues = Form.useWatch([], form);
const [overviewTab, setOverviewTab] = useState("description");
const [submitting, setSubmitting] = useState(false);
const [loadingSubmission, setLoadingSubmission] = useState(true);
const [isEditingSubmittedOrder, setIsEditingSubmittedOrder] = useState(false);
@@ -414,26 +414,33 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
amount: existingSubmission ? savedEstimatedAmount : null,
};
const hasPayment = paymentMethods.length > 0;
// Payment details are only revealed once the participant has placed an order.
const showPaymentSection = hasPayment && !!existingSubmission;
const paymentTabs = hasPayment
? [
{
key: "payment",
label: "Payment",
icon: <WalletOutlined />,
children: (
<Space direction="vertical" size={12} style={{ width: "100%" }}>
{!existingSubmission && (
<Alert
type="info"
showIcon
message="Place your order first so the correct amount is shown here."
/>
)}
<PaymentMethodsAccordion methods={paymentMethods} context={paymentContext} />
</Space>
),
},
]
: [];
const openPaymentSection = () => {
document.getElementById(PAYMENT_CARD_ID)?.scrollIntoView({ behavior: "smooth", block: "start" });
const openPaymentTab = () => {
setOverviewTab("payment");
document.getElementById(OVERVIEW_CARD_ID)?.scrollIntoView({ behavior: "smooth", block: "start" });
};
const isPaid = !!existingSubmission?.paid;
const progressSteps = [
{ title: "Place order", description: "Choose and submit" },
...(hasPayment ? [{ title: "Pay", description: "Transfer the amount" }] : []),
{ title: "Done", description: hasPayment ? "Payment received" : "Order placed" },
];
const doneStep = progressSteps.length - 1;
const currentStep = !existingSubmission
? 0
: hasPayment && !isPaid
? 1
: doneStep;
const canShowSubmittedResult =
!!existingSubmission && !isEditingSubmittedOrder;
const showSummaryView = isClosed || canShowSubmittedResult;
@@ -478,18 +485,14 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
/>
)}
<Steps
size="small"
responsive
current={currentStep}
status={currentStep === doneStep && existingSubmission ? "finish" : "process"}
items={progressSteps}
/>
<ReadOnlyOrderOverviewCard
id={OVERVIEW_CARD_ID}
order={liveOrder}
config={config}
hideMenu
extraTabs={paymentTabs}
activeTabKey={overviewTab}
onTabChange={setOverviewTab}
/>
<Card
@@ -523,23 +526,7 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
savedOrderString={savedOrderString}
savedEstimatedTotal={savedEstimatedTotal}
isClosed={isClosed}
/>
)}
{showSummaryView && showPaymentSection && !isPaid && (
<Alert
type="info"
showIcon
icon={<WalletOutlined />}
message={
savedEstimatedTotal
? `Your estimated total is ${savedEstimatedTotal}`
: "Payment details are available for this order"
}
action={
<Button size="small" type="primary" onClick={openPaymentSection}>
How to pay
</Button>
}
onHowToPay={hasPayment ? openPaymentTab : undefined}
/>
)}
{!showSummaryView && (
@@ -620,13 +607,6 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
</Space>
</Card>
{showPaymentSection && (
<PaymentDetailsCard
id={PAYMENT_CARD_ID}
payment={{ methods: paymentMethods }}
context={paymentContext}
/>
)}
</Space>
</div>
</AsyncContent>