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 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"; import type { Submission } from "../../lib/types";
const { Text } = Typography; const { Text } = Typography;
@@ -22,12 +23,17 @@ export default function ParticipantSubmissionSummary({
savedOrderString, savedOrderString,
savedEstimatedTotal, savedEstimatedTotal,
isClosed, isClosed,
onHowToPay,
}: { }: {
submission: Submission | null; submission: Submission | null;
savedOrderString: string; savedOrderString: string;
savedEstimatedTotal: string | null; savedEstimatedTotal: string | null;
isClosed: boolean; isClosed: boolean;
/** Shown as a "How to pay" button when payment details exist. */
onHowToPay?: () => void;
}) { }) {
const { token } = theme.useToken();
if (!submission) { if (!submission) {
return ( return (
<Result <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 ( return (
<Result <Flex vertical gap={8}>
status="success" <Flex justify="space-between" align="center" wrap="wrap" gap={8}>
title="Order submitted" <Text strong>
subTitle={ <CheckCircleFilled style={{ color: token.colorSuccess }} /> Order submitted
<Space direction="vertical"> </Text>
<Tooltip title="Your submitted order"> <Flex gap={4} wrap="wrap">
<Text code copyable> {statuses.map(({ label, done }) => (
{savedOrderString} <Tag
</Text> key={label}
</Tooltip> 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 && ( {savedEstimatedTotal && (
<Tooltip title="Rough estimate based on configured item prices. Final amount may differ."> <Tooltip title="Rough estimate based on configured item prices. Final amount may differ.">
<Text type="secondary">Estimated total: {savedEstimatedTotal}</Text> <Text type="secondary">Estimated total: {savedEstimatedTotal}</Text>
</Tooltip> </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 }}> <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> </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, Popconfirm,
Row, Row,
Space, Space,
Steps,
Tooltip, Tooltip,
Typography, Typography,
message, message,
@@ -20,7 +19,7 @@ import { navigateTo } from "../lib/routing";
import { apiService } from "../lib/services"; import { apiService } from "../lib/services";
import LoadingSkeleton from "../components/utils/LoadingSkeleton"; import LoadingSkeleton from "../components/utils/LoadingSkeleton";
import ReadOnlyOrderOverviewCard from "../components/orders/ReadOnlyOrderOverviewCard"; 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 { normalizePaymentDetails, type PaymentContext } from "../lib/paymentDetails";
import { import {
buildFormattedOrderString, buildFormattedOrderString,
@@ -42,7 +41,7 @@ import ParticipantDraftSummaryAlert from "../components/views/ParticipantDraftSu
const { Text, Link } = Typography; const { Text, Link } = Typography;
const PAYMENT_CARD_ID = "order-payment"; const OVERVIEW_CARD_ID = "order-overview";
type SubmissionValues = { type SubmissionValues = {
choices: NormalizedOrderSelections; choices: NormalizedOrderSelections;
@@ -91,6 +90,7 @@ type ParticipantLoadData = {
export default function ParticipantView({ orderId }: { orderId: string }) { export default function ParticipantView({ orderId }: { orderId: string }) {
const [form] = Form.useForm<FormModel>(); const [form] = Form.useForm<FormModel>();
const watchedValues = Form.useWatch([], form); const watchedValues = Form.useWatch([], form);
const [overviewTab, setOverviewTab] = useState("description");
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [loadingSubmission, setLoadingSubmission] = useState(true); const [loadingSubmission, setLoadingSubmission] = useState(true);
const [isEditingSubmittedOrder, setIsEditingSubmittedOrder] = useState(false); const [isEditingSubmittedOrder, setIsEditingSubmittedOrder] = useState(false);
@@ -414,26 +414,33 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
amount: existingSubmission ? savedEstimatedAmount : null, amount: existingSubmission ? savedEstimatedAmount : null,
}; };
const hasPayment = paymentMethods.length > 0; const hasPayment = paymentMethods.length > 0;
// Payment details are only revealed once the participant has placed an order. const paymentTabs = hasPayment
const showPaymentSection = hasPayment && !!existingSubmission; ? [
{
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 = () => { const openPaymentTab = () => {
document.getElementById(PAYMENT_CARD_ID)?.scrollIntoView({ behavior: "smooth", block: "start" }); 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 = const canShowSubmittedResult =
!!existingSubmission && !isEditingSubmittedOrder; !!existingSubmission && !isEditingSubmittedOrder;
const showSummaryView = isClosed || canShowSubmittedResult; 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 <ReadOnlyOrderOverviewCard
id={OVERVIEW_CARD_ID}
order={liveOrder} order={liveOrder}
config={config} config={config}
hideMenu hideMenu
extraTabs={paymentTabs}
activeTabKey={overviewTab}
onTabChange={setOverviewTab}
/> />
<Card <Card
@@ -523,23 +526,7 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
savedOrderString={savedOrderString} savedOrderString={savedOrderString}
savedEstimatedTotal={savedEstimatedTotal} savedEstimatedTotal={savedEstimatedTotal}
isClosed={isClosed} isClosed={isClosed}
/> onHowToPay={hasPayment ? openPaymentTab : undefined}
)}
{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>
}
/> />
)} )}
{!showSummaryView && ( {!showSummaryView && (
@@ -620,13 +607,6 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
</Space> </Space>
</Card> </Card>
{showPaymentSection && (
<PaymentDetailsCard
id={PAYMENT_CARD_ID}
payment={{ methods: paymentMethods }}
context={paymentContext}
/>
)}
</Space> </Space>
</div> </div>
</AsyncContent> </AsyncContent>