Add SummaryStep, PaymentStep, ConfirmationStep (wizard steps 13-15)
SummaryStep (step 13): - Accordion sections with edit IconButtons linking back to each step - dl/dt/dd definition list for label-value pairs - Total bar with prominent price display (aria-live) - Share plan icon button, deposit display - Pre-planning: "Save your plan" CTA; at-need: "Confirm" CTA PaymentStep (step 14): - Payment plan (full/deposit) shown before method (amount before how) - ToggleButtonGroup for plan + method selection - Card: PayWay iframe slot with placeholder; Bank: account details display - Terms checkbox with service agreement + privacy links - Security reassurance (lock icon, no-surprise copy) ConfirmationStep (step 15): - Terminal page — no back button, no progress indicator - At-need: "submitted" + callback timeframe + arranger contact - Pre-planning: "saved" + return-anytime + family-ready copy - Muted success icon (not celebratory), next-steps link buttons - VenueCard selected prop also staged (from step 7 work) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ConfirmationStep } from './ConfirmationStep';
|
||||||
|
import { Navigation } from '../../organisms/Navigation';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FALogo = () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-full.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-short.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nav = (
|
||||||
|
<Navigation
|
||||||
|
logo={<FALogo />}
|
||||||
|
items={[
|
||||||
|
{ label: 'FAQ', href: '/faq' },
|
||||||
|
{ label: 'Contact Us', href: '/contact' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const meta: Meta<typeof ConfirmationStep> = {
|
||||||
|
title: 'Pages/ConfirmationStep',
|
||||||
|
component: ConfirmationStep,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ConfirmationStep>;
|
||||||
|
|
||||||
|
// ─── At-need (default) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** At-need confirmation — arranger will call */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<ConfirmationStep
|
||||||
|
email="jane.smith@example.com"
|
||||||
|
phone="0412 345 678"
|
||||||
|
callbackTimeframe="within 2 hours"
|
||||||
|
onViewPlan={() => alert('View plan')}
|
||||||
|
nextSteps={[
|
||||||
|
{ label: 'Start detailed arrangement', onClick: () => alert('Arrangement') },
|
||||||
|
{ label: 'Go to dashboard', onClick: () => alert('Dashboard') },
|
||||||
|
]}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Pre-planning ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Pre-planning confirmation — plan saved, return anytime */
|
||||||
|
export const PrePlanning: Story = {
|
||||||
|
render: () => (
|
||||||
|
<ConfirmationStep
|
||||||
|
isPrePlanning
|
||||||
|
email="jane.smith@example.com"
|
||||||
|
onViewPlan={() => alert('View plan')}
|
||||||
|
nextSteps={[{ label: 'Go to dashboard', onClick: () => alert('Dashboard') }]}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Minimal ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Minimal — no next steps, no view plan */
|
||||||
|
export const Minimal: Story = {
|
||||||
|
render: () => <ConfirmationStep email="jane.smith@example.com" navigation={nav} />,
|
||||||
|
};
|
||||||
152
src/components/pages/ConfirmationStep/ConfirmationStep.tsx
Normal file
152
src/components/pages/ConfirmationStep/ConfirmationStep.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Next step link shown on the confirmation page */
|
||||||
|
export interface ConfirmationLink {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the ConfirmationStep page component */
|
||||||
|
export interface ConfirmationStepProps {
|
||||||
|
/** Whether this is a pre-planning flow (different copy) */
|
||||||
|
isPrePlanning?: boolean;
|
||||||
|
/** User's email address (shown in confirmation text) */
|
||||||
|
email?: string;
|
||||||
|
/** User's phone number (shown in at-need confirmation) */
|
||||||
|
phone?: string;
|
||||||
|
/** Expected callback timeframe (at-need) */
|
||||||
|
callbackTimeframe?: string;
|
||||||
|
/** Navigation links to next steps */
|
||||||
|
nextSteps?: ConfirmationLink[];
|
||||||
|
/** Callback for "View your plan" */
|
||||||
|
onViewPlan?: () => void;
|
||||||
|
/** Navigation bar */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** Hide the help bar */
|
||||||
|
hideHelpBar?: boolean;
|
||||||
|
/** MUI sx prop */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 15 — Confirmation / Plan View for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Terminal confirmation page. Different copy for at-need vs pre-planning.
|
||||||
|
* Confirming and orienting — not celebratory.
|
||||||
|
*
|
||||||
|
* At-need: "Your arrangement has been submitted" + callback info
|
||||||
|
* Pre-planning: "Your plan has been saved" + return-anytime info
|
||||||
|
*
|
||||||
|
* No progress indicator. No back button. Links to post-plan flows.
|
||||||
|
*
|
||||||
|
* Pure presentation component — props in, callbacks out.
|
||||||
|
*
|
||||||
|
* Spec: documentation/steps/steps/15_view.yaml
|
||||||
|
*/
|
||||||
|
export const ConfirmationStep: React.FC<ConfirmationStepProps> = ({
|
||||||
|
isPrePlanning = false,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
callbackTimeframe = 'within 2 hours',
|
||||||
|
nextSteps = [],
|
||||||
|
onViewPlan,
|
||||||
|
navigation,
|
||||||
|
hideHelpBar,
|
||||||
|
sx,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout variant="centered-form" navigation={navigation} hideHelpBar={hideHelpBar} sx={sx}>
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
{/* Success icon — muted, not celebratory */}
|
||||||
|
<CheckCircleOutlineIcon
|
||||||
|
sx={{
|
||||||
|
fontSize: 64,
|
||||||
|
color: 'var(--fa-color-brand-500)',
|
||||||
|
mb: 3,
|
||||||
|
opacity: 0.8,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Heading */}
|
||||||
|
<Typography variant="display3" component="h1" sx={{ mb: 2 }} tabIndex={-1}>
|
||||||
|
{isPrePlanning ? 'Your plan has been saved' : 'Your arrangement has been submitted'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Body text */}
|
||||||
|
{isPrePlanning ? (
|
||||||
|
<Box sx={{ maxWidth: 480, mx: 'auto', mb: 4 }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
You can return and update it anytime.
|
||||||
|
{email && ` We've sent a copy to ${email}.`}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
|
||||||
|
When the time comes, your family can contact us and we'll have everything ready.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ maxWidth: 480, mx: 'auto', mb: 4 }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
A funeral arranger will call you
|
||||||
|
{phone && ` on ${phone}`} {callbackTimeframe} to confirm the details.
|
||||||
|
{email && ` A confirmation has been sent to ${email}.`}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
|
||||||
|
If you need to make changes before then, call us on{' '}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="tel:1300000000"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
1300 000 000
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View plan CTA */}
|
||||||
|
{onViewPlan && (
|
||||||
|
<Button variant="contained" size="large" onClick={onViewPlan} sx={{ mb: 4 }}>
|
||||||
|
View your plan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Next steps */}
|
||||||
|
{nextSteps.length > 0 && (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, alignItems: 'center' }}>
|
||||||
|
{nextSteps.map((link) => (
|
||||||
|
<Button
|
||||||
|
key={link.label}
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
onClick={link.onClick}
|
||||||
|
href={link.href}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfirmationStep.displayName = 'ConfirmationStep';
|
||||||
|
export default ConfirmationStep;
|
||||||
2
src/components/pages/ConfirmationStep/index.ts
Normal file
2
src/components/pages/ConfirmationStep/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ConfirmationStep, default } from './ConfirmationStep';
|
||||||
|
export type { ConfirmationStepProps, ConfirmationLink } from './ConfirmationStep';
|
||||||
184
src/components/pages/PaymentStep/PaymentStep.stories.tsx
Normal file
184
src/components/pages/PaymentStep/PaymentStep.stories.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { PaymentStep } from './PaymentStep';
|
||||||
|
import type { PaymentStepValues, PaymentStepErrors } from './PaymentStep';
|
||||||
|
import { Navigation } from '../../organisms/Navigation';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FALogo = () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-full.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-short.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nav = (
|
||||||
|
<Navigation
|
||||||
|
logo={<FALogo />}
|
||||||
|
items={[
|
||||||
|
{ label: 'FAQ', href: '/faq' },
|
||||||
|
{ label: 'Contact Us', href: '/contact' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultValues: PaymentStepValues = {
|
||||||
|
paymentPlan: null,
|
||||||
|
paymentMethod: null,
|
||||||
|
termsAccepted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const meta: Meta<typeof PaymentStep> = {
|
||||||
|
title: 'Pages/PaymentStep',
|
||||||
|
component: PaymentStep,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof PaymentStep>;
|
||||||
|
|
||||||
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full interactive payment flow */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [values, setValues] = useState<PaymentStepValues>({ ...defaultValues });
|
||||||
|
const [errors, setErrors] = useState<PaymentStepErrors>({});
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const newErrors: PaymentStepErrors = {};
|
||||||
|
if (!values.paymentPlan) newErrors.paymentPlan = 'Please choose a payment option.';
|
||||||
|
if (!values.paymentMethod) newErrors.paymentMethod = 'Please choose a payment method.';
|
||||||
|
if (!values.termsAccepted)
|
||||||
|
newErrors.termsAccepted = 'Please review and accept the service agreement to continue.';
|
||||||
|
setErrors(newErrors);
|
||||||
|
if (Object.keys(newErrors).length === 0) alert('Payment confirmed!');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentStep
|
||||||
|
values={values}
|
||||||
|
onChange={(v) => {
|
||||||
|
setValues(v);
|
||||||
|
setErrors({});
|
||||||
|
}}
|
||||||
|
onConfirmPayment={handleConfirm}
|
||||||
|
onBack={() => alert('Back to summary')}
|
||||||
|
errors={errors}
|
||||||
|
totalPrice={9850}
|
||||||
|
depositAmount={2000}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Card selected ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Card payment selected — shows payment form placeholder */
|
||||||
|
export const CardPayment: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [values, setValues] = useState<PaymentStepValues>({
|
||||||
|
paymentPlan: 'full',
|
||||||
|
paymentMethod: 'Card',
|
||||||
|
termsAccepted: false,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<PaymentStep
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
onConfirmPayment={() => alert('Confirm')}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
totalPrice={9850}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Bank transfer selected ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Bank transfer selected — shows account details */
|
||||||
|
export const BankTransfer: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [values, setValues] = useState<PaymentStepValues>({
|
||||||
|
paymentPlan: 'deposit',
|
||||||
|
paymentMethod: 'Bank',
|
||||||
|
termsAccepted: true,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<PaymentStep
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
onConfirmPayment={() => alert('Confirm')}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
totalPrice={9850}
|
||||||
|
depositAmount={2000}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Validation errors ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** All errors showing */
|
||||||
|
export const WithErrors: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [values, setValues] = useState<PaymentStepValues>({ ...defaultValues });
|
||||||
|
return (
|
||||||
|
<PaymentStep
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
onConfirmPayment={() => {}}
|
||||||
|
errors={{
|
||||||
|
paymentPlan: 'Please choose a payment option.',
|
||||||
|
paymentMethod: 'Please choose a payment method.',
|
||||||
|
termsAccepted: 'Please review and accept the service agreement to continue.',
|
||||||
|
}}
|
||||||
|
totalPrice={9850}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Processing ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Payment processing */
|
||||||
|
export const Processing: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [values, setValues] = useState<PaymentStepValues>({
|
||||||
|
paymentPlan: 'full',
|
||||||
|
paymentMethod: 'Card',
|
||||||
|
termsAccepted: true,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<PaymentStep
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
onConfirmPayment={() => {}}
|
||||||
|
loading
|
||||||
|
totalPrice={9850}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
332
src/components/pages/PaymentStep/PaymentStep.tsx
Normal file
332
src/components/pages/PaymentStep/PaymentStep.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
|
import { ToggleButtonGroup } from '../../atoms/ToggleButtonGroup';
|
||||||
|
import { Collapse } from '../../atoms/Collapse';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Payment plan choice */
|
||||||
|
export type PaymentPlan = 'full' | 'deposit' | null;
|
||||||
|
|
||||||
|
/** Payment method choice */
|
||||||
|
export type PaymentMethod = 'Card' | 'Bank' | null;
|
||||||
|
|
||||||
|
/** Form values for the payment step */
|
||||||
|
export interface PaymentStepValues {
|
||||||
|
/** Full payment or deposit */
|
||||||
|
paymentPlan: PaymentPlan;
|
||||||
|
/** Card or bank transfer */
|
||||||
|
paymentMethod: PaymentMethod;
|
||||||
|
/** Terms accepted */
|
||||||
|
termsAccepted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Field-level error messages */
|
||||||
|
export interface PaymentStepErrors {
|
||||||
|
paymentPlan?: string;
|
||||||
|
paymentMethod?: string;
|
||||||
|
termsAccepted?: string;
|
||||||
|
card?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the PaymentStep page component */
|
||||||
|
export interface PaymentStepProps {
|
||||||
|
/** Current form values */
|
||||||
|
values: PaymentStepValues;
|
||||||
|
/** Callback when any field value changes */
|
||||||
|
onChange: (values: PaymentStepValues) => void;
|
||||||
|
/** Callback when Confirm Payment is clicked */
|
||||||
|
onConfirmPayment: () => void;
|
||||||
|
/** Callback for back navigation */
|
||||||
|
onBack?: () => void;
|
||||||
|
/** Field-level validation errors */
|
||||||
|
errors?: PaymentStepErrors;
|
||||||
|
/** Whether the button is in a loading/processing state */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Total amount */
|
||||||
|
totalPrice: number;
|
||||||
|
/** Deposit amount */
|
||||||
|
depositAmount?: number;
|
||||||
|
/** Bank account details for transfer */
|
||||||
|
bankDetails?: {
|
||||||
|
accountName: string;
|
||||||
|
bsb: string;
|
||||||
|
accountNumber: string;
|
||||||
|
reference?: string;
|
||||||
|
};
|
||||||
|
/** Card payment iframe slot (PayWay integration) */
|
||||||
|
cardFormSlot?: React.ReactNode;
|
||||||
|
/** Navigation bar */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** Progress stepper */
|
||||||
|
progressStepper?: React.ReactNode;
|
||||||
|
/** Hide the help bar */
|
||||||
|
hideHelpBar?: boolean;
|
||||||
|
/** MUI sx prop */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 14 — Payment for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Collect payment via credit card or bank transfer. Supports full
|
||||||
|
* payment or deposit. Hidden for pre-planning users.
|
||||||
|
*
|
||||||
|
* Payment plan shown before method so users know the amount first.
|
||||||
|
* Card details via PayWay iframe (slot). Bank transfer shows account info.
|
||||||
|
*
|
||||||
|
* Pure presentation component — props in, callbacks out.
|
||||||
|
*
|
||||||
|
* Spec: documentation/steps/steps/14_payment.yaml
|
||||||
|
*/
|
||||||
|
export const PaymentStep: React.FC<PaymentStepProps> = ({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
onConfirmPayment,
|
||||||
|
onBack,
|
||||||
|
errors,
|
||||||
|
loading = false,
|
||||||
|
totalPrice,
|
||||||
|
depositAmount = 2000,
|
||||||
|
bankDetails = {
|
||||||
|
accountName: 'Funeral Arranger Services Pty Ltd',
|
||||||
|
bsb: '112-879',
|
||||||
|
accountNumber: '481 449 385',
|
||||||
|
},
|
||||||
|
cardFormSlot,
|
||||||
|
navigation,
|
||||||
|
progressStepper,
|
||||||
|
hideHelpBar,
|
||||||
|
sx,
|
||||||
|
}) => {
|
||||||
|
const payingAmount = values.paymentPlan === 'deposit' ? depositAmount : totalPrice;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WizardLayout
|
||||||
|
variant="centered-form"
|
||||||
|
navigation={navigation}
|
||||||
|
progressStepper={progressStepper}
|
||||||
|
showBackLink={!!onBack}
|
||||||
|
backLabel="Back"
|
||||||
|
onBack={onBack}
|
||||||
|
hideHelpBar={hideHelpBar}
|
||||||
|
sx={sx}
|
||||||
|
>
|
||||||
|
{/* Page heading */}
|
||||||
|
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
|
||||||
|
Payment
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 4 }}>
|
||||||
|
<LockOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Your payment is processed securely. You won't be charged more than the total shown.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ─── Amount display ─── */}
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mb: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||||
|
{values.paymentPlan === 'deposit' ? 'Deposit amount' : 'Total due'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="display3" color="primary" aria-live="polite" aria-atomic="true">
|
||||||
|
${payingAmount.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
{values.paymentPlan === 'deposit' && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
Remaining balance: ${(totalPrice - depositAmount).toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
noValidate
|
||||||
|
onSubmit={(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onConfirmPayment();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ─── Payment plan ─── */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
label="Payment option"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'full',
|
||||||
|
label: 'Pay in full',
|
||||||
|
description: `Pay $${totalPrice.toLocaleString('en-AU')} now`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'deposit',
|
||||||
|
label: 'Pay a deposit',
|
||||||
|
description: `Pay $${depositAmount.toLocaleString('en-AU')} now, balance arranged with provider`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={values.paymentPlan}
|
||||||
|
onChange={(v) => onChange({ ...values, paymentPlan: v as PaymentPlan })}
|
||||||
|
error={!!errors?.paymentPlan}
|
||||||
|
helperText={errors?.paymentPlan}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ─── Payment method ─── */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
label="Payment method"
|
||||||
|
options={[
|
||||||
|
{ value: 'Card', label: 'Credit or debit card' },
|
||||||
|
{ value: 'Bank', label: 'Bank transfer' },
|
||||||
|
]}
|
||||||
|
value={values.paymentMethod}
|
||||||
|
onChange={(v) => onChange({ ...values, paymentMethod: v as PaymentMethod })}
|
||||||
|
error={!!errors?.paymentMethod}
|
||||||
|
helperText={errors?.paymentMethod}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ─── Card form (PayWay iframe slot) ─── */}
|
||||||
|
<Collapse in={values.paymentMethod === 'Card'}>
|
||||||
|
<Box sx={{ mb: 3, minHeight: 200 }} role="region" aria-label="Payment details">
|
||||||
|
{cardFormSlot || (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: 200,
|
||||||
|
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Secure card payment form will appear here
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
{errors?.card && (
|
||||||
|
<Typography variant="body2" color="error" sx={{ mt: 1 }} role="alert">
|
||||||
|
{errors.card}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* ─── Bank transfer details ─── */}
|
||||||
|
<Collapse in={values.paymentMethod === 'Bank'}>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ p: 3, mb: 3 }}
|
||||||
|
role="region"
|
||||||
|
aria-label="Bank transfer details"
|
||||||
|
>
|
||||||
|
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||||
|
Bank transfer details
|
||||||
|
</Typography>
|
||||||
|
<Box component="dl" sx={{ m: 0 }}>
|
||||||
|
{[
|
||||||
|
{ label: 'Account name', value: bankDetails.accountName },
|
||||||
|
{ label: 'BSB', value: bankDetails.bsb },
|
||||||
|
{ label: 'Account number', value: bankDetails.accountNumber },
|
||||||
|
].map(({ label, value }) => (
|
||||||
|
<Box key={label} sx={{ display: 'flex', justifyContent: 'space-between', py: 1 }}>
|
||||||
|
<Typography component="dt" variant="body2" color="text.secondary">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography component="dd" variant="body1" sx={{ m: 0, fontFamily: 'monospace' }}>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
{bankDetails.reference && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||||
|
Reference: {bankDetails.reference}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* ─── Terms checkbox ─── */}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={values.termsAccepted}
|
||||||
|
onChange={(e) => onChange({ ...values, termsAccepted: e.target.checked })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="body2">
|
||||||
|
I agree to the{' '}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="#"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
service agreement
|
||||||
|
</Box>{' '}
|
||||||
|
and{' '}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="#"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
privacy policy
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
sx={{ mb: 1, alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0.5 } }}
|
||||||
|
/>
|
||||||
|
{errors?.termsAccepted && (
|
||||||
|
<Typography variant="body2" color="error" sx={{ mb: 2, ml: 4 }} role="alert">
|
||||||
|
{errors.termsAccepted}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button type="submit" variant="contained" size="large" loading={loading}>
|
||||||
|
Confirm payment
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaymentStep.displayName = 'PaymentStep';
|
||||||
|
export default PaymentStep;
|
||||||
8
src/components/pages/PaymentStep/index.ts
Normal file
8
src/components/pages/PaymentStep/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { PaymentStep, default } from './PaymentStep';
|
||||||
|
export type {
|
||||||
|
PaymentStepProps,
|
||||||
|
PaymentStepValues,
|
||||||
|
PaymentStepErrors,
|
||||||
|
PaymentPlan,
|
||||||
|
PaymentMethod,
|
||||||
|
} from './PaymentStep';
|
||||||
150
src/components/pages/SummaryStep/SummaryStep.stories.tsx
Normal file
150
src/components/pages/SummaryStep/SummaryStep.stories.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { SummaryStep } from './SummaryStep';
|
||||||
|
import type { SummarySection } from './SummaryStep';
|
||||||
|
import { Navigation } from '../../organisms/Navigation';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const FALogo = () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-full.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="/brandlogo/logo-short.svg"
|
||||||
|
alt="Funeral Arranger"
|
||||||
|
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nav = (
|
||||||
|
<Navigation
|
||||||
|
logo={<FALogo />}
|
||||||
|
items={[
|
||||||
|
{ label: 'FAQ', href: '/faq' },
|
||||||
|
{ label: 'Contact Us', href: '/contact' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sampleSections: SummarySection[] = [
|
||||||
|
{
|
||||||
|
id: 'provider',
|
||||||
|
title: 'Funeral Provider',
|
||||||
|
editStepId: 'providers',
|
||||||
|
items: [
|
||||||
|
{ label: 'Provider', value: 'H. Parsons Funeral Directors' },
|
||||||
|
{ label: 'Package', value: 'Essential Service Package', price: 4950 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'venue',
|
||||||
|
title: 'Service Venue',
|
||||||
|
editStepId: 'venue',
|
||||||
|
items: [
|
||||||
|
{ label: 'Venue', value: 'West Chapel, Strathfield', price: 900 },
|
||||||
|
{ label: 'Photo presentation', value: 'Included', price: 150 },
|
||||||
|
{ label: 'Livestream', value: 'Included', price: 200 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crematorium',
|
||||||
|
title: 'Crematorium',
|
||||||
|
editStepId: 'crematorium',
|
||||||
|
items: [
|
||||||
|
{ label: 'Crematorium', value: 'Warrill Park Crematorium', price: 850 },
|
||||||
|
{ label: 'Following hearse', value: 'Yes' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'coffin',
|
||||||
|
title: 'Coffin',
|
||||||
|
editStepId: 'coffins',
|
||||||
|
items: [
|
||||||
|
{ label: 'Coffin', value: 'Cedar Classic', price: 2800 },
|
||||||
|
{ label: 'Handles', value: 'Brass Bar Handle', price: 0 },
|
||||||
|
{ label: 'Lining', value: 'White Satin', price: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'services',
|
||||||
|
title: 'Additional Services',
|
||||||
|
editStepId: 'additional_services',
|
||||||
|
items: [
|
||||||
|
{ label: 'Funeral announcement', value: 'Included' },
|
||||||
|
{ label: 'Bearing', value: 'Family and friends' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const meta: Meta<typeof SummaryStep> = {
|
||||||
|
title: 'Pages/SummaryStep',
|
||||||
|
component: SummaryStep,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof SummaryStep>;
|
||||||
|
|
||||||
|
// ─── At-need (default) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full summary for at-need flow */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<SummaryStep
|
||||||
|
sections={sampleSections}
|
||||||
|
totalPrice={9850}
|
||||||
|
depositAmount={2000}
|
||||||
|
onConfirm={() => alert('Confirmed — proceed to payment')}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
onSaveAndExit={() => alert('Save')}
|
||||||
|
onEdit={(stepId) => alert(`Edit: ${stepId}`)}
|
||||||
|
onShare={() => alert('Share plan')}
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Pre-planning ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Pre-planning variant — "Save your plan" CTA, no payment */
|
||||||
|
export const PrePlanning: Story = {
|
||||||
|
render: () => (
|
||||||
|
<SummaryStep
|
||||||
|
sections={sampleSections}
|
||||||
|
totalPrice={9850}
|
||||||
|
onConfirm={() => alert('Plan saved')}
|
||||||
|
onBack={() => alert('Back')}
|
||||||
|
onEdit={(stepId) => alert(`Edit: ${stepId}`)}
|
||||||
|
onShare={() => alert('Share plan')}
|
||||||
|
isPrePlanning
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Loading ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Confirm button loading */
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: () => (
|
||||||
|
<SummaryStep
|
||||||
|
sections={sampleSections}
|
||||||
|
totalPrice={9850}
|
||||||
|
onConfirm={() => {}}
|
||||||
|
loading
|
||||||
|
navigation={nav}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
264
src/components/pages/SummaryStep/SummaryStep.tsx
Normal file
264
src/components/pages/SummaryStep/SummaryStep.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { IconButton } from '../../atoms/IconButton';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** A single line item in the summary */
|
||||||
|
export interface SummaryLineItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
price?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A section in the summary (e.g. Provider, Venue, Coffin) */
|
||||||
|
export interface SummarySection {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
items: SummaryLineItem[];
|
||||||
|
/** Step ID to navigate back to for editing */
|
||||||
|
editStepId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the SummaryStep page component */
|
||||||
|
export interface SummaryStepProps {
|
||||||
|
/** Summary sections */
|
||||||
|
sections: SummarySection[];
|
||||||
|
/** Total cost */
|
||||||
|
totalPrice: number;
|
||||||
|
/** Deposit amount (if applicable) */
|
||||||
|
depositAmount?: number;
|
||||||
|
/** Callback when Confirm is clicked */
|
||||||
|
onConfirm: () => void;
|
||||||
|
/** Callback for back navigation */
|
||||||
|
onBack?: () => void;
|
||||||
|
/** Callback for save-and-exit */
|
||||||
|
onSaveAndExit?: () => void;
|
||||||
|
/** Callback when edit is clicked on a section */
|
||||||
|
onEdit?: (stepId: string) => void;
|
||||||
|
/** Callback for sharing the plan */
|
||||||
|
onShare?: () => void;
|
||||||
|
/** Whether the Confirm button is in a loading state */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Whether this is a pre-planning flow */
|
||||||
|
isPrePlanning?: boolean;
|
||||||
|
/** Navigation bar */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** Progress stepper */
|
||||||
|
progressStepper?: React.ReactNode;
|
||||||
|
/** Hide the help bar */
|
||||||
|
hideHelpBar?: boolean;
|
||||||
|
/** MUI sx prop */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 13 — Summary / Review for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Complete summary of the funeral plan with all selections and pricing.
|
||||||
|
* Accordion sections with edit links back to each step. Total bar at bottom.
|
||||||
|
*
|
||||||
|
* For pre-planning: CTA is "Save your plan" instead of "Confirm".
|
||||||
|
* For at-need: CTA is "Confirm and continue to payment".
|
||||||
|
*
|
||||||
|
* Pure presentation component — props in, callbacks out.
|
||||||
|
*
|
||||||
|
* Spec: documentation/steps/steps/13_summary.yaml
|
||||||
|
*/
|
||||||
|
export const SummaryStep: React.FC<SummaryStepProps> = ({
|
||||||
|
sections,
|
||||||
|
totalPrice,
|
||||||
|
depositAmount,
|
||||||
|
onConfirm,
|
||||||
|
onBack,
|
||||||
|
onSaveAndExit,
|
||||||
|
onEdit,
|
||||||
|
onShare,
|
||||||
|
loading = false,
|
||||||
|
isPrePlanning = false,
|
||||||
|
navigation,
|
||||||
|
progressStepper,
|
||||||
|
hideHelpBar,
|
||||||
|
sx,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout
|
||||||
|
variant="centered-form"
|
||||||
|
navigation={navigation}
|
||||||
|
progressStepper={progressStepper}
|
||||||
|
showBackLink={!!onBack}
|
||||||
|
backLabel="Back"
|
||||||
|
onBack={onBack}
|
||||||
|
hideHelpBar={hideHelpBar}
|
||||||
|
sx={sx}
|
||||||
|
>
|
||||||
|
{/* Header with share */}
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="display3" component="h1" tabIndex={-1}>
|
||||||
|
Review your plan
|
||||||
|
</Typography>
|
||||||
|
{onShare && (
|
||||||
|
<IconButton aria-label="Share this plan via email" onClick={onShare} size="medium">
|
||||||
|
<ShareOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
Check everything looks right before confirming.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
You can edit any section by tapping the edit icon.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* ─── Summary sections ─── */}
|
||||||
|
{sections.map((section) => (
|
||||||
|
<Accordion
|
||||||
|
key={section.id}
|
||||||
|
defaultExpanded
|
||||||
|
disableGutters
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: '8px !important',
|
||||||
|
mb: 2,
|
||||||
|
'&:before': { display: 'none' },
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 3, py: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1, gap: 1 }}>
|
||||||
|
<Typography variant="h5" sx={{ flex: 1 }}>
|
||||||
|
{section.title}
|
||||||
|
</Typography>
|
||||||
|
{section.editStepId && onEdit && (
|
||||||
|
<IconButton
|
||||||
|
aria-label={`Edit ${section.title}`}
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit(section.editStepId!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ px: 3, pb: 3 }}>
|
||||||
|
<Box component="dl" sx={{ m: 0 }}>
|
||||||
|
{section.items.map((item, i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
py: 1,
|
||||||
|
borderBottom: i < section.items.length - 1 ? 1 : 0,
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography component="dt" variant="body2" color="text.secondary">
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography component="dd" variant="body1" sx={{ m: 0 }}>
|
||||||
|
{item.value}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{item.price != null && (
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="primary"
|
||||||
|
sx={{ ml: 2, whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
${item.price.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* ─── Total bar ─── */}
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mt: 3,
|
||||||
|
mb: 4,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5">Total cost</Typography>
|
||||||
|
{depositAmount != null && !isPrePlanning && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Deposit: ${depositAmount.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="display3" color="primary" aria-live="polite" aria-atomic="true">
|
||||||
|
${totalPrice.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Payment reassurance */}
|
||||||
|
{!isPrePlanning && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2, textAlign: 'center' }}>
|
||||||
|
You won't be charged until you complete the next step.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: { xs: 'column-reverse', sm: 'row' },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{onSaveAndExit ? (
|
||||||
|
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
|
||||||
|
Save and continue later
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Box />
|
||||||
|
)}
|
||||||
|
<Button variant="contained" size="large" loading={loading} onClick={onConfirm}>
|
||||||
|
{isPrePlanning ? 'Save your plan' : 'Confirm'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SummaryStep.displayName = 'SummaryStep';
|
||||||
|
export default SummaryStep;
|
||||||
2
src/components/pages/SummaryStep/index.ts
Normal file
2
src/components/pages/SummaryStep/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { SummaryStep, default } from './SummaryStep';
|
||||||
|
export type { SummaryStepProps, SummarySection, SummaryLineItem } from './SummaryStep';
|
||||||
Reference in New Issue
Block a user