799 lines
30 KiB
JavaScript
799 lines
30 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Form, Input, Button, DatePicker, Steps, Radio, Card, message, Alert, Descriptions, Breadcrumb, Typography, Modal, Row, Col, Select, Tag } from 'antd';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import { HomeOutlined, CreditCardOutlined, PlusOutlined, CheckCircleFilled } from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import api from '../utils/api';
|
|
import { colors, spacing, fontSize, commonStyles } from '../theme';
|
|
|
|
const { Option } = Select;
|
|
|
|
const { Step } = Steps;
|
|
const { TextArea } = Input;
|
|
const { Title } = Typography;
|
|
|
|
// Card type colors
|
|
const cardTypeColors = {
|
|
'Visa': '#1a1f71',
|
|
'Mastercard': '#eb001b',
|
|
'American Express': '#006fcf',
|
|
'Discover': '#ff6000',
|
|
'default': colors.primary,
|
|
};
|
|
|
|
const Order = () => {
|
|
const navigate = useNavigate();
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [pricing, setPricing] = useState({ price_per_gallon: 0, date: null });
|
|
const [totalPrice, setTotalPrice] = useState(0);
|
|
const [selectedGallons, setSelectedGallons] = useState(100);
|
|
const [selectedDate, setSelectedDate] = useState(dayjs().add(1, 'day'));
|
|
const [customerTown, setCustomerTown] = useState(null);
|
|
const [serviceType, setServiceType] = useState(null); // null, 'same_day', 'emergency'
|
|
const [primeSelected, setPrimeSelected] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
gallons_ordered: 100,
|
|
expected_delivery_date: dayjs().add(1, 'day'),
|
|
dispatcher_notes: '',
|
|
payment_type: 1, // credit
|
|
});
|
|
const [notesLength, setNotesLength] = useState(0);
|
|
|
|
// Saved cards state
|
|
const [savedCards, setSavedCards] = useState([]);
|
|
const [loadingCards, setLoadingCards] = useState(true);
|
|
const [selectedCardId, setSelectedCardId] = useState(null);
|
|
const [addCardModalVisible, setAddCardModalVisible] = useState(false);
|
|
const [addCardLoading, setAddCardLoading] = useState(false);
|
|
const [addCardForm] = Form.useForm();
|
|
|
|
// Generate year options for card expiration
|
|
const currentYear = new Date().getFullYear();
|
|
const yearOptions = Array.from({ length: 11 }, (_, i) => currentYear + i);
|
|
|
|
// Fetch pricing on component mount and set up auto-refresh
|
|
useEffect(() => {
|
|
const fetchPricing = async () => {
|
|
try {
|
|
const response = await api.get('/info/pricing/current');
|
|
setPricing(response.data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch pricing:', error);
|
|
}
|
|
};
|
|
|
|
fetchPricing();
|
|
|
|
// Auto-refresh pricing every 30 seconds
|
|
const interval = setInterval(fetchPricing, 30000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// Fetch customer town on component mount
|
|
useEffect(() => {
|
|
const fetchCustomerData = async () => {
|
|
try {
|
|
const response = await api.get('/auth/me');
|
|
setCustomerTown(response.data.customer_town);
|
|
} catch (error) {
|
|
console.error('Failed to fetch customer data:', error);
|
|
}
|
|
};
|
|
|
|
fetchCustomerData();
|
|
}, []);
|
|
|
|
// Fetch saved cards on component mount
|
|
useEffect(() => {
|
|
const fetchSavedCards = async () => {
|
|
setLoadingCards(true);
|
|
try {
|
|
const response = await api.get('/payment/cards');
|
|
setSavedCards(response.data);
|
|
// Auto-select default card if exists
|
|
const defaultCard = response.data.find(card => card.main_card);
|
|
if (defaultCard) {
|
|
setSelectedCardId(defaultCard.id);
|
|
} else if (response.data.length > 0) {
|
|
setSelectedCardId(response.data[0].id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch saved cards:', error);
|
|
} finally {
|
|
setLoadingCards(false);
|
|
}
|
|
};
|
|
|
|
fetchSavedCards();
|
|
}, []);
|
|
|
|
// Calculate total price when gallons, pricing, service type, or prime changes
|
|
useEffect(() => {
|
|
const basePrice = pricing.price_per_gallon * formData.gallons_ordered;
|
|
const surcharge = getSurcharge();
|
|
const calculatedTotal = basePrice + surcharge;
|
|
setTotalPrice(calculatedTotal);
|
|
}, [pricing.price_per_gallon, formData.gallons_ordered, serviceType, primeSelected]);
|
|
|
|
const next = () => {
|
|
setCurrentStep(currentStep + 1);
|
|
};
|
|
|
|
const prev = () => {
|
|
setCurrentStep(currentStep - 1);
|
|
};
|
|
|
|
const handleGallonsChange = (value) => {
|
|
setFormData({ ...formData, gallons_ordered: value });
|
|
setSelectedGallons(value);
|
|
};
|
|
|
|
const handleDateChange = (date) => {
|
|
setFormData({ ...formData, expected_delivery_date: date });
|
|
setSelectedDate(date);
|
|
};
|
|
|
|
const handleNotesChange = (e) => {
|
|
setFormData({ ...formData, dispatcher_notes: e.target.value });
|
|
setNotesLength(e.target.value.length);
|
|
};
|
|
|
|
const handlePaymentChange = (e) => {
|
|
setFormData({ ...formData, payment_type: e.target.value });
|
|
};
|
|
|
|
// Towns where 100 gallons is restricted
|
|
const restrictedTowns = ['Sutton', 'Millbury', 'Paxton', 'Grafton'];
|
|
|
|
// Check if same day service is available (6am-2pm)
|
|
const isSameDayAvailable = () => {
|
|
const now = dayjs();
|
|
const hour = now.hour();
|
|
return hour >= 6 && hour < 14; // 6am to 2pm (14:00)
|
|
};
|
|
|
|
// Get surcharge based on service type and prime
|
|
const getSurcharge = () => {
|
|
let surcharge = 0;
|
|
if (serviceType === 'same_day') surcharge += 45;
|
|
if (serviceType === 'emergency') surcharge += 250;
|
|
if (primeSelected) surcharge += serviceType === 'emergency' ? 50 : 25;
|
|
return surcharge;
|
|
};
|
|
|
|
// Handle same day service selection
|
|
const handleSameDaySelect = () => {
|
|
if (serviceType === 'same_day') {
|
|
// Deselect same day
|
|
setServiceType(null);
|
|
handleDateChange(dayjs().add(1, 'day')); // Reset to tomorrow
|
|
} else {
|
|
// Select same day
|
|
setServiceType('same_day');
|
|
handleDateChange(dayjs()); // Set to today
|
|
}
|
|
};
|
|
|
|
// Handle emergency service selection
|
|
const handleEmergencySelect = () => {
|
|
if (serviceType === 'emergency') {
|
|
// Deselect emergency
|
|
setServiceType(null);
|
|
handleDateChange(dayjs().add(1, 'day')); // Reset to tomorrow
|
|
handleGallonsChange(100); // Reset to default gallons
|
|
} else {
|
|
// Select emergency
|
|
setServiceType('emergency');
|
|
handleDateChange(dayjs()); // Set to today
|
|
handleGallonsChange(225); // Force 225 gallons
|
|
}
|
|
};
|
|
|
|
// Helper function to mask card number
|
|
const maskCardNumber = (cardNumber) => {
|
|
if (!cardNumber) return '';
|
|
const digits = cardNumber.replace(/\s/g, '');
|
|
const last4 = digits.slice(-4);
|
|
return `**** **** **** ${last4}`;
|
|
};
|
|
|
|
// Handle add new card
|
|
const handleAddCard = () => {
|
|
addCardForm.resetFields();
|
|
setAddCardModalVisible(true);
|
|
};
|
|
|
|
const handleAddCardSubmit = async () => {
|
|
try {
|
|
const values = await addCardForm.validateFields();
|
|
setAddCardLoading(true);
|
|
|
|
const response = await api.post('/payment/cards', {
|
|
card_number: values.card_number.replace(/\s/g, ''),
|
|
name_on_card: values.name_on_card,
|
|
expiration_month: values.expiration_month,
|
|
expiration_year: values.expiration_year,
|
|
security_number: values.security_number,
|
|
zip_code: values.zip_code,
|
|
});
|
|
|
|
message.success('Card added successfully');
|
|
setAddCardModalVisible(false);
|
|
addCardForm.resetFields();
|
|
|
|
// Refresh cards and select the new one
|
|
const cardsResponse = await api.get('/payment/cards');
|
|
setSavedCards(cardsResponse.data);
|
|
setSelectedCardId(response.data.id);
|
|
} catch (error) {
|
|
if (error.errorFields) {
|
|
return;
|
|
}
|
|
const errorMsg = error.response?.data?.detail || 'Failed to add card';
|
|
message.error(errorMsg);
|
|
console.error('Error adding card:', error);
|
|
} finally {
|
|
setAddCardLoading(false);
|
|
}
|
|
};
|
|
|
|
// Get selected card details
|
|
const getSelectedCard = () => {
|
|
return savedCards.find(card => card.id === selectedCardId);
|
|
};
|
|
|
|
const onFinish = async () => {
|
|
setLoading(true);
|
|
try {
|
|
if (formData.payment_type === 1) {
|
|
// Validate card selection
|
|
if (!selectedCardId) {
|
|
message.error('Please select a payment card or add a new one');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Process credit card payment using saved card
|
|
const paymentData = {
|
|
amount: totalPrice,
|
|
card_id: selectedCardId,
|
|
};
|
|
|
|
const paymentResponse = await api.post('/payment/process', paymentData);
|
|
|
|
if (paymentResponse.data.status === 'approved') {
|
|
message.success(`Payment approved! Transaction ID: ${paymentResponse.data.auth_net_transaction_id}`, 4.5);
|
|
|
|
// Now create the delivery
|
|
const deliveryData = {
|
|
gallons_ordered: formData.gallons_ordered,
|
|
expected_delivery_date: formData.expected_delivery_date.format('YYYY-MM-DD'),
|
|
dispatcher_notes: formData.dispatcher_notes,
|
|
payment_type: formData.payment_type,
|
|
prime: primeSelected ? 1 : 0,
|
|
same_day: serviceType === 'same_day' ? 1 : 0,
|
|
emergency: serviceType === 'emergency' ? 1 : 0,
|
|
pre_charge_amount: totalPrice,
|
|
};
|
|
|
|
const deliveryResponse = await api.post('/order/deliveries', deliveryData);
|
|
message.success('Delivery created successfully!', 4.5);
|
|
|
|
navigate('/');
|
|
|
|
// Reset form
|
|
setFormData({
|
|
gallons_ordered: 100,
|
|
expected_delivery_date: dayjs().add(1, 'day'),
|
|
dispatcher_notes: '',
|
|
payment_type: 1,
|
|
});
|
|
setServiceType(null);
|
|
setPrimeSelected(false);
|
|
setCurrentStep(0);
|
|
} else {
|
|
message.error('Payment was declined. Please check your card details.', 4.5);
|
|
}
|
|
} else {
|
|
// Cash payment - just create delivery
|
|
const data = {
|
|
gallons_ordered: formData.gallons_ordered,
|
|
expected_delivery_date: formData.expected_delivery_date.format('YYYY-MM-DD'),
|
|
dispatcher_notes: formData.dispatcher_notes,
|
|
payment_type: formData.payment_type,
|
|
prime: primeSelected ? 1 : 0,
|
|
same_day: serviceType === 'same_day' ? 1 : 0,
|
|
emergency: serviceType === 'emergency' ? 1 : 0,
|
|
pre_charge_amount: totalPrice,
|
|
};
|
|
const response = await api.post('/order/deliveries', data);
|
|
message.success('Delivery created successfully!', 4.5);
|
|
|
|
navigate('/');
|
|
|
|
// Reset form
|
|
setFormData({
|
|
gallons_ordered: 100,
|
|
expected_delivery_date: dayjs().add(1, 'day'),
|
|
dispatcher_notes: '',
|
|
payment_type: 1,
|
|
});
|
|
setServiceType(null);
|
|
setPrimeSelected(false);
|
|
setCurrentStep(0);
|
|
}
|
|
} catch (error) {
|
|
message.error('Failed to process order: ' + error.response?.data?.detail || error.message, 4.5);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const steps = [
|
|
{
|
|
title: 'Order Details',
|
|
content: (
|
|
<div>
|
|
{/* Part 1: Gallons and Date */}
|
|
<div style={{ marginBottom: 24, padding: 16, backgroundColor: '#fff', borderRadius: 8, border: '1px solid #e8e8e8' }}>
|
|
<Form.Item label="Gallons Ordered" rules={[{ required: true, message: 'Please enter gallons ordered' }]}>
|
|
<div style={{ marginBottom: 8, display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
|
{[100, 125, 150, 175, 200, 225, 250].map(gallons => (
|
|
<Button
|
|
key={gallons}
|
|
type={selectedGallons === gallons ? "primary" : "default"}
|
|
onClick={() => handleGallonsChange(gallons)}
|
|
disabled={
|
|
(gallons === 100 && customerTown && restrictedTowns.some(town => town.toLowerCase() === customerTown.toLowerCase())) ||
|
|
(serviceType === 'emergency' && gallons !== 225)
|
|
}
|
|
size="small"
|
|
>
|
|
{gallons}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
value={formData.gallons_ordered}
|
|
onChange={(e) => handleGallonsChange(parseInt(e.target.value))}
|
|
min={100}
|
|
disabled={serviceType === 'emergency'}
|
|
/>
|
|
{formData.gallons_ordered < 100 && (
|
|
<div style={{ color: 'red' }}>200$ charge below 100 gallons</div>
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item label="Delivery Date" rules={[{ required: true, message: 'Please select a delivery date' }]}>
|
|
<div style={{ marginBottom: 8, display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
|
<Button
|
|
type={selectedDate.isSame(dayjs().add(1, 'day'), 'day') ? "primary" : "default"}
|
|
onClick={() => handleDateChange(dayjs().add(1, 'day'))}
|
|
disabled={serviceType !== null}
|
|
size="small"
|
|
>
|
|
Tomorrow
|
|
</Button>
|
|
<Button
|
|
type={selectedDate.isSame(dayjs().add(2, 'day'), 'day') ? "primary" : "default"}
|
|
onClick={() => handleDateChange(dayjs().add(2, 'day'))}
|
|
disabled={serviceType !== null}
|
|
size="small"
|
|
>
|
|
In 2 days
|
|
</Button>
|
|
<Button
|
|
type={selectedDate.isSame(dayjs().add(3, 'day'), 'day') ? "primary" : "default"}
|
|
onClick={() => handleDateChange(dayjs().add(3, 'day'))}
|
|
disabled={serviceType !== null}
|
|
size="small"
|
|
>
|
|
In 3 days
|
|
</Button>
|
|
</div>
|
|
<DatePicker
|
|
value={formData.expected_delivery_date}
|
|
onChange={handleDateChange}
|
|
disabledDate={(current) => current && current < dayjs().startOf('day')}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
|
|
{/* Part 2: Service Options */}
|
|
<div style={{ marginBottom: 24, padding: 16, backgroundColor: '#fff', borderRadius: 8, border: '1px solid #e8e8e8' }}>
|
|
<div style={{ fontWeight: 'bold', marginBottom: 8 }}>Service Options</div>
|
|
<div style={{ marginBottom: 10 }}>
|
|
{isSameDayAvailable() && (
|
|
<Button
|
|
type={serviceType === 'same_day' ? "primary" : "default"}
|
|
onClick={handleSameDaySelect}
|
|
style={{ marginRight: 10 }}
|
|
>
|
|
Same Day (+$45)
|
|
</Button>
|
|
)}
|
|
{!isSameDayAvailable() && (
|
|
<div>
|
|
<div style={{ marginBottom: 8, fontSize: '14px', color: '#666' }}>
|
|
After hours immediate delivery
|
|
</div>
|
|
<Button
|
|
type={serviceType === 'emergency' ? "primary" : "default"}
|
|
onClick={handleEmergencySelect}
|
|
>
|
|
Emergency (+$250)
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{serviceType === 'same_day' && (
|
|
<div style={{ color: 'blue', fontSize: '12px' }}>
|
|
Same day service selected - delivery today
|
|
</div>
|
|
)}
|
|
{serviceType === 'emergency' && (
|
|
<div style={{ color: 'blue', fontSize: '12px' }}>
|
|
Emergency service selected - 225 gallons only
|
|
</div>
|
|
)}
|
|
<div style={{ marginTop: 16 }}>
|
|
<div style={{ marginBottom: 8, fontSize: '14px', color: '#666' }}>
|
|
Ran out of oil? You will need a prime to restart your system
|
|
</div>
|
|
<Button
|
|
type={primeSelected ? "primary" : "default"}
|
|
onClick={() => setPrimeSelected(!primeSelected)}
|
|
>
|
|
Prime (+${serviceType === 'emergency' ? 50 : 25})
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Part 3: Notes */}
|
|
<div style={{ padding: 16, backgroundColor: '#fff', borderRadius: 8, border: '1px solid #e8e8e8' }}>
|
|
<Form.Item label="Notes for office" layout="vertical" style={{ width: '100%' }}>
|
|
<TextArea
|
|
value={formData.dispatcher_notes}
|
|
onChange={handleNotesChange}
|
|
placeholder="Notes for the dispatcher"
|
|
style={{ width: '100%', borderColor: notesLength > 250 ? 'red' : undefined }}
|
|
/>
|
|
</Form.Item>
|
|
<div style={{ color: notesLength > 250 ? 'red' : 'green', fontSize: '12px', textAlign: 'right' }}>
|
|
{notesLength}/250
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: 'Payment',
|
|
content: (
|
|
<div>
|
|
<Form.Item label="Payment Method">
|
|
<Radio.Group onChange={handlePaymentChange} value={formData.payment_type}>
|
|
<Radio value={0}>Cash</Radio>
|
|
<Radio value={1}>Credit</Radio>
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
{formData.payment_type === 1 && (
|
|
<div>
|
|
{loadingCards ? (
|
|
<Card style={{ textAlign: 'center', padding: spacing.lg }}>
|
|
<div>Loading saved cards...</div>
|
|
</Card>
|
|
) : savedCards.length === 0 ? (
|
|
<Card style={{ textAlign: 'center', padding: spacing.xl }}>
|
|
<CreditCardOutlined style={{ fontSize: 48, color: colors.gray[300], marginBottom: spacing.md }} />
|
|
<div style={{ marginBottom: spacing.md, color: colors.text.secondary }}>
|
|
No saved payment methods
|
|
</div>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddCard}>
|
|
Add New Card
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<div>
|
|
<div style={{ marginBottom: spacing.sm, fontWeight: 500 }}>Select a payment card:</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: spacing.sm, marginBottom: spacing.md }}>
|
|
{savedCards.map((card) => {
|
|
const cardColor = cardTypeColors[card.type_of_card] || cardTypeColors.default;
|
|
const isSelected = selectedCardId === card.id;
|
|
return (
|
|
<Card
|
|
key={card.id}
|
|
size="small"
|
|
style={{
|
|
cursor: 'pointer',
|
|
border: isSelected ? `2px solid ${colors.primary}` : '1px solid #d9d9d9',
|
|
backgroundColor: isSelected ? '#f0f5ff' : '#fff',
|
|
}}
|
|
onClick={() => setSelectedCardId(card.id)}
|
|
styles={{ body: { padding: spacing.sm } }}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: spacing.sm }}>
|
|
<CreditCardOutlined style={{ fontSize: 24, color: cardColor }} />
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>
|
|
{card.type_of_card || 'Card'} •••• {card.last_four_digits}
|
|
{card.main_card && (
|
|
<Tag color="gold" style={{ marginLeft: spacing.xs }}>Default</Tag>
|
|
)}
|
|
</div>
|
|
<div style={{ fontSize: fontSize.xs, color: colors.text.secondary }}>
|
|
{card.name_on_card} | Expires {card.expiration_month}/{card.expiration_year?.slice(-2)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{isSelected && (
|
|
<CheckCircleFilled style={{ fontSize: 20, color: colors.primary }} />
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
<Button icon={<PlusOutlined />} onClick={handleAddCard}>
|
|
Add New Card
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{formData.payment_type === 0 && (
|
|
<Card style={{ textAlign: 'center', padding: spacing.md }}>
|
|
<div style={{ color: colors.text.secondary }}>
|
|
Cash payment - pay the driver upon delivery
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: 'Review',
|
|
content: (
|
|
<div>
|
|
<Descriptions title="Order Summary" bordered column={1}>
|
|
<Descriptions.Item label="Gallons">{formData.gallons_ordered}</Descriptions.Item>
|
|
<Descriptions.Item label="Price per Gallon">${pricing.price_per_gallon.toFixed(2)}</Descriptions.Item>
|
|
{serviceType && (
|
|
<Descriptions.Item label="Service Surcharge">${serviceType === 'same_day' ? 45 : 250} ({serviceType === 'same_day' ? 'Same Day' : 'Emergency'})</Descriptions.Item>
|
|
)}
|
|
{primeSelected && (
|
|
<Descriptions.Item label="Prime Surcharge">${serviceType === 'emergency' ? 50 : 25}</Descriptions.Item>
|
|
)}
|
|
<Descriptions.Item label="Total Amount">${totalPrice.toFixed(2)}</Descriptions.Item>
|
|
<Descriptions.Item label="Delivery Date">{formData.expected_delivery_date.format('YYYY-MM-DD')}</Descriptions.Item>
|
|
<Descriptions.Item label="Notes">{formData.dispatcher_notes || 'None'}</Descriptions.Item>
|
|
<Descriptions.Item label="Payment Method">{formData.payment_type === 0 ? 'Cash' : 'Credit Card'}</Descriptions.Item>
|
|
</Descriptions>
|
|
{formData.payment_type === 1 && getSelectedCard() && (
|
|
<Card
|
|
style={{
|
|
marginTop: 16,
|
|
width: 300,
|
|
background: `linear-gradient(135deg, ${cardTypeColors[getSelectedCard().type_of_card] || cardTypeColors.default} 0%, ${cardTypeColors[getSelectedCard().type_of_card] || cardTypeColors.default}dd 100%)`,
|
|
color: colors.white,
|
|
borderRadius: 12,
|
|
}}
|
|
>
|
|
<div style={{ fontSize: fontSize.sm, marginBottom: spacing.xs }}>{getSelectedCard().type_of_card || 'Credit Card'}</div>
|
|
<div style={{ fontSize: '18px', fontWeight: 'bold', fontFamily: 'monospace', letterSpacing: 2 }}>
|
|
•••• •••• •••• {getSelectedCard().last_four_digits}
|
|
</div>
|
|
<div style={{ marginTop: spacing.sm }}>{getSelectedCard().name_on_card}</div>
|
|
<div style={{ marginTop: 4, fontSize: '14px' }}>
|
|
{getSelectedCard().expiration_month}/{getSelectedCard().expiration_year?.slice(-2)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div style={{ padding: '4px 0' }}>
|
|
<Breadcrumb style={{ marginBottom: 16 }}>
|
|
<Breadcrumb.Item>
|
|
<Link to="/">
|
|
<HomeOutlined /> Home
|
|
</Link>
|
|
</Breadcrumb.Item>
|
|
<Breadcrumb.Item>Order Oil</Breadcrumb.Item>
|
|
</Breadcrumb>
|
|
|
|
<Title level={1} style={{ marginBottom: 16 }}>Order Oil</Title>
|
|
|
|
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
|
|
{/* Dynamic Pricing Display */}
|
|
<Alert
|
|
message={`Current Oil Price: $${pricing.price_per_gallon.toFixed(2)} per gallon`}
|
|
description={`Total for ${formData.gallons_ordered} gallons: $${totalPrice.toFixed(2)}`}
|
|
type="info"
|
|
showIcon
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
|
|
<Steps current={currentStep}>
|
|
{steps.map((item) => (
|
|
<Step key={item.title} title={item.title} />
|
|
))}
|
|
</Steps>
|
|
<div style={{ marginTop: 8 }}>
|
|
<Card style={commonStyles.cardGray} bodyStyle={{ padding: spacing.sm }}>
|
|
{steps[currentStep].content}
|
|
<div style={{ marginTop: 24 }}>
|
|
{currentStep < steps.length - 1 && (
|
|
<Button type="primary" onClick={next} disabled={notesLength > 250}>
|
|
Next
|
|
</Button>
|
|
)}
|
|
{currentStep === steps.length - 1 && (
|
|
<Button type="primary" onClick={onFinish} loading={loading} disabled={notesLength > 250}>
|
|
Submit
|
|
</Button>
|
|
)}
|
|
{currentStep > 0 && (
|
|
<Button style={{ marginLeft: 8 }} onClick={prev}>
|
|
Previous
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
{/* Add Card Modal */}
|
|
<Modal
|
|
title="Add New Card"
|
|
open={addCardModalVisible}
|
|
onOk={handleAddCardSubmit}
|
|
onCancel={() => {
|
|
setAddCardModalVisible(false);
|
|
addCardForm.resetFields();
|
|
}}
|
|
confirmLoading={addCardLoading}
|
|
okText="Add Card"
|
|
cancelText="Cancel"
|
|
destroyOnClose
|
|
>
|
|
<Form
|
|
form={addCardForm}
|
|
layout="vertical"
|
|
style={{ marginTop: spacing.md }}
|
|
>
|
|
<Form.Item
|
|
name="name_on_card"
|
|
label="Name on Card"
|
|
rules={[
|
|
{ required: true, message: 'Please enter the name on card' },
|
|
{ min: 2, message: 'Name must be at least 2 characters' },
|
|
]}
|
|
>
|
|
<Input placeholder="John Doe" autoComplete="cc-name" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="card_number"
|
|
label="Card Number"
|
|
rules={[
|
|
{ required: true, message: 'Please enter your card number' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve();
|
|
const digits = value.replace(/\s/g, '');
|
|
if (!/^\d+$/.test(digits)) {
|
|
return Promise.reject('Card number must contain only digits');
|
|
}
|
|
if (digits.length < 13 || digits.length > 19) {
|
|
return Promise.reject('Card number must be between 13 and 19 digits');
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="1234 5678 9012 3456"
|
|
maxLength={19}
|
|
autoComplete="cc-number"
|
|
onChange={(e) => {
|
|
// Auto-format with spaces every 4 digits
|
|
const value = e.target.value.replace(/\s/g, '').replace(/\D/g, '');
|
|
const formatted = value.match(/.{1,4}/g)?.join(' ') || value;
|
|
addCardForm.setFieldValue('card_number', formatted);
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Row gutter={spacing.sm}>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="expiration_month"
|
|
label="Month"
|
|
rules={[{ required: true, message: 'Required' }]}
|
|
>
|
|
<Select placeholder="MM" autoComplete="cc-exp-month">
|
|
{Array.from({ length: 12 }, (_, i) => {
|
|
const month = String(i + 1).padStart(2, '0');
|
|
return (
|
|
<Option key={month} value={month}>
|
|
{month}
|
|
</Option>
|
|
);
|
|
})}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="expiration_year"
|
|
label="Year"
|
|
rules={[{ required: true, message: 'Required' }]}
|
|
>
|
|
<Select placeholder="YYYY" autoComplete="cc-exp-year">
|
|
{yearOptions.map((year) => (
|
|
<Option key={year} value={String(year)}>
|
|
{year}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="security_number"
|
|
label="CVV"
|
|
rules={[
|
|
{ required: true, message: 'Required' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve();
|
|
if (!/^\d{3,4}$/.test(value)) {
|
|
return Promise.reject('3-4 digits');
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="123"
|
|
maxLength={4}
|
|
autoComplete="cc-csc"
|
|
style={{ textAlign: 'center' }}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Form.Item
|
|
name="zip_code"
|
|
label="Billing Zip Code"
|
|
rules={[
|
|
{ required: true, message: 'Please enter your billing zip code' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve();
|
|
if (!/^\d{5}(-\d{4})?$/.test(value)) {
|
|
return Promise.reject('Enter a valid US zip code');
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<Input placeholder="12345" maxLength={10} autoComplete="billing postal-code" />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Order;
|