payment ux refinement
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m37s
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m37s
This commit is contained in:
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user