384 lines
17 KiB
JavaScript
384 lines
17 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { Row, Col, Card, Typography, Button, Space, message, Table, Image, Grid, Skeleton } from 'antd';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import api, { API_BASE_URL } from '../utils/api';
|
|
import { colors, spacing, fontSize, buttonStyles, commonStyles } from '../theme';
|
|
|
|
const { Title } = Typography;
|
|
const { useBreakpoint } = Grid;
|
|
|
|
const deliveriesColumns = [
|
|
{
|
|
title: 'Date',
|
|
dataIndex: 'delivery_date',
|
|
key: 'delivery_date',
|
|
render: (date) => date ? new Date(date).toLocaleDateString() : 'N/A'
|
|
},
|
|
{
|
|
title: 'Gallons',
|
|
dataIndex: 'gallons',
|
|
key: 'gallons',
|
|
render: (gallons) => gallons ? `${gallons} gal` : 'N/A'
|
|
},
|
|
{
|
|
title: 'Price',
|
|
dataIndex: 'customer_price',
|
|
key: 'customer_price',
|
|
render: (price) => price ? `$${price.toFixed(2)}` : 'N/A'
|
|
},
|
|
{
|
|
title: 'Status',
|
|
dataIndex: 'delivery_status',
|
|
key: 'delivery_status',
|
|
render: (status) => {
|
|
if (status === 'Pending') return 'pending cc charge';
|
|
if (status === 'Finalized') return 'Delivered/Charged';
|
|
if (status === 'Waiting') return 'waiting for delivery';
|
|
return status;
|
|
}
|
|
},
|
|
{
|
|
title: 'Payment Type',
|
|
dataIndex: 'payment_type',
|
|
key: 'payment_type'
|
|
}
|
|
];
|
|
|
|
function Index() {
|
|
const navigate = useNavigate();
|
|
const isLoggedIn = !!localStorage.getItem('token');
|
|
const [userData, setUserData] = useState(null);
|
|
const [deliveries, setDeliveries] = useState([]);
|
|
const [tankImages, setTankImages] = useState([]);
|
|
const [pricing, setPricing] = useState(null);
|
|
const [loadingUser, setLoadingUser] = useState(true);
|
|
const [loadingDeliveries, setLoadingDeliveries] = useState(true);
|
|
const [loadingTankImages, setLoadingTankImages] = useState(true);
|
|
const [loadingPricing, setLoadingPricing] = useState(true);
|
|
const screens = useBreakpoint();
|
|
|
|
const fetchTankImages = async (accountNumber) => {
|
|
setLoadingTankImages(true);
|
|
try {
|
|
const response = await api.get(`/auth/tank-images/${accountNumber}`);
|
|
|
|
// Get the latest image set (first one after sorting)
|
|
const imageGroups = response.data.image_sets.map(set => ({
|
|
date: set.date,
|
|
images: set.images.map(img => `${API_BASE_URL}${img}`)
|
|
}));
|
|
|
|
// Sort by date descending (newest first)
|
|
imageGroups.sort((a, b) => {
|
|
const parseDate = (dateStr) => {
|
|
if (dateStr.includes('_')) {
|
|
const [datePart, timePart] = dateStr.split('_');
|
|
const [year, month, day] = datePart.split('-');
|
|
const [hour, minute, second] = timePart.split('-');
|
|
return new Date(year, month - 1, day, hour, minute, second);
|
|
} else {
|
|
return new Date(dateStr);
|
|
}
|
|
};
|
|
return parseDate(b.date) - parseDate(a.date);
|
|
});
|
|
|
|
setTankImages(imageGroups[0]?.images || []);
|
|
} catch (error) {
|
|
console.error('Failed to load tank images:', error);
|
|
// No fallback to example images on main page
|
|
} finally {
|
|
setLoadingTankImages(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isLoggedIn) {
|
|
const fetchUserData = async () => {
|
|
setLoadingUser(true);
|
|
try {
|
|
const response = await api.get('/auth/me');
|
|
setUserData(response.data);
|
|
await fetchTankImages(response.data.account_number);
|
|
} catch (error) {
|
|
message.error('Failed to load user data');
|
|
console.error('Error fetching user data:', error);
|
|
} finally {
|
|
setLoadingUser(false);
|
|
}
|
|
};
|
|
|
|
const fetchDeliveries = async () => {
|
|
setLoadingDeliveries(true);
|
|
try {
|
|
const response = await api.get('/info/deliveries');
|
|
setDeliveries(response.data);
|
|
} catch (error) {
|
|
message.error('Failed to load deliveries');
|
|
console.error('Error fetching deliveries:', error);
|
|
} finally {
|
|
setLoadingDeliveries(false);
|
|
}
|
|
};
|
|
|
|
const fetchPricing = async () => {
|
|
setLoadingPricing(true);
|
|
try {
|
|
const response = await api.get('/info/pricing/current');
|
|
setPricing(response.data);
|
|
} catch (error) {
|
|
console.error('Failed to load pricing:', error);
|
|
} finally {
|
|
setLoadingPricing(false);
|
|
}
|
|
};
|
|
|
|
fetchUserData();
|
|
fetchDeliveries();
|
|
fetchPricing();
|
|
} else {
|
|
// Reset loading states when not logged in
|
|
setLoadingUser(false);
|
|
setLoadingDeliveries(false);
|
|
setLoadingTankImages(false);
|
|
setLoadingPricing(false);
|
|
}
|
|
}, [isLoggedIn]);
|
|
|
|
if (!isLoggedIn) {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '60vh', padding: '0 16px' }}>
|
|
<Title level={2} style={{ textAlign: 'center' }}>Welcome to Oil Customer Gateway</Title>
|
|
<Space direction="vertical" size="large" style={{ width: '100%', maxWidth: '400px' }}>
|
|
<div style={{ display: 'flex', flexDirection: screens.xs ? 'column' : 'row', alignItems: screens.xs ? 'flex-start' : 'center', gap: screens.xs ? '8px' : '16px' }}>
|
|
<Button type="primary" size={screens.xs ? 'default' : 'large'} block={screens.xs} onClick={() => navigate('/login')}>Login</Button>
|
|
<span style={{ fontSize: screens.xs ? '14px' : '16px' }}>For customers with an existing online account</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: screens.xs ? 'column' : 'row', alignItems: screens.xs ? 'flex-start' : 'center', gap: screens.xs ? '8px' : '16px' }}>
|
|
<Button type="primary" size={screens.xs ? 'default' : 'large'} block={screens.xs} onClick={() => navigate('/register')}>Register</Button>
|
|
<span style={{ fontSize: screens.xs ? '14px' : '16px' }}>For existing customers who haven't signed up for online access</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: screens.xs ? 'column' : 'row', alignItems: screens.xs ? 'flex-start' : 'center', gap: screens.xs ? '8px' : '16px' }}>
|
|
<Button type="primary" size={screens.xs ? 'default' : 'large'} block={screens.xs} onClick={() => navigate('/new')}>New Customer</Button>
|
|
<span style={{ fontSize: screens.xs ? '14px' : '16px' }}>For new customers to Auburn Oil</span>
|
|
</div>
|
|
</Space>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ maxWidth: '100%', overflow: 'hidden' }}>
|
|
{/* Row 1: Customer Information and Order Oil */}
|
|
<Row gutter={screens.xs ? 8 : 16} style={{ marginBottom: 16 }}>
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
|
<Card
|
|
title="Customer Information"
|
|
bordered={true}
|
|
style={commonStyles.cardGray}
|
|
extra={!loadingUser && userData && <Link to="/edit-customer" style={commonStyles.link}>Edit</Link>}
|
|
>
|
|
{loadingUser ? (
|
|
<>
|
|
<div style={{ textAlign: 'center', marginBottom: 8 }}>
|
|
<Skeleton.Input active style={{ width: 120, height: screens.xs ? 24 : 32 }} />
|
|
</div>
|
|
<Skeleton active paragraph={{ rows: 4 }} title={false} />
|
|
</>
|
|
) : userData ? (
|
|
<>
|
|
<div style={{ textAlign: 'center', marginBottom: 8 }}>
|
|
<Title level={1} style={{ margin: 0, fontSize: screens.xs ? '24px' : '32px' }}>{userData.account_number}</Title>
|
|
</div>
|
|
<p style={{ marginBottom: 4, fontSize: screens.xs ? '14px' : '16px', wordBreak: 'break-word' }}><strong>Name:</strong> {userData.customer_first_name} {userData.customer_last_name}</p>
|
|
<p style={{ marginBottom: 4, fontSize: screens.xs ? '14px' : '16px', wordBreak: 'break-word' }}><strong>Address:</strong> {userData.customer_address}, {userData.customer_town}, {userData.customer_state} {userData.customer_zip}</p>
|
|
<p style={{ marginBottom: 4, fontSize: screens.xs ? '14px' : '16px', wordBreak: 'break-word' }}><strong>Phone:</strong> {userData.customer_phone_number}</p>
|
|
<p style={{ marginBottom: 0, fontSize: screens.xs ? '14px' : '16px', wordBreak: 'break-word' }}><strong>Email:</strong> {userData.email}</p>
|
|
</>
|
|
) : (
|
|
<p style={{ fontSize: screens.xs ? '14px' : '16px' }}>Failed to load customer information.</p>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
|
<Card
|
|
title="Order Oil"
|
|
bordered={true}
|
|
style={{ height: screens.xs ? 'auto' : '100%', minHeight: screens.xs ? 120 : 'auto' }}
|
|
styles={{ body: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: screens.xs ? 80 : 120, gap: '16px' } }}
|
|
>
|
|
{loadingPricing ? (
|
|
<Skeleton.Input active style={{ width: 100 }} />
|
|
) : pricing ? (
|
|
<Title level={1} style={{ margin: 0, fontSize: screens.xs ? '24px' : '32px', color: 'black' }}>Todays Price: ${pricing.price_per_gallon.toFixed(2)}</Title>
|
|
) : (
|
|
<Title level={1} style={{ margin: 0, fontSize: screens.xs ? '24px' : '32px', color: 'black' }}>Todays Price: N/A</Title>
|
|
)}
|
|
<Button
|
|
type="primary"
|
|
size={screens.xs ? 'default' : 'large'}
|
|
style={screens.xs ? buttonStyles.ctaMobile : buttonStyles.ctaLarge}
|
|
onClick={() => navigate('/order')}
|
|
>
|
|
Place Order
|
|
</Button>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Row 2: Tank Images and Emergency Delivery Info */}
|
|
<Row gutter={screens.xs ? 8 : 16} style={{ marginBottom: 16 }}>
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
|
<Card
|
|
title="Upload / Edit Tank Images"
|
|
bordered={true}
|
|
style={!loadingTankImages && tankImages.length === 0 ? { borderColor: 'red' } : {}}
|
|
>
|
|
{loadingTankImages ? (
|
|
<>
|
|
<Row gutter={8} justify="center" style={{ marginBottom: 16 }}>
|
|
{[1, 2, 3].map((i) => (
|
|
<Col key={i}>
|
|
<Skeleton.Image active style={{ width: screens.xs ? 120 : 100, height: screens.xs ? 120 : 100 }} />
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
<Skeleton.Input active size="small" style={{ width: 140 }} />
|
|
</>
|
|
) : tankImages.length > 0 ? (
|
|
<>
|
|
<Row gutter={8} justify="center" style={{ marginBottom: 16 }}>
|
|
{tankImages.map((image, index) => (
|
|
<Col key={index}>
|
|
<Image width={screens.xs ? 120 : 100} src={image} alt={`Tank ${index + 1}`} />
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
<Link to="/tank" style={{ color: colors.text.link, fontSize: screens.xs ? '14px' : '16px' }}>Upload / Edit images</Link>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Typography.Text type="warning" style={{ fontSize: screens.xs ? '14px' : '16px' }}>Please upload tank images</Typography.Text>
|
|
<br />
|
|
<br />
|
|
<Link to="/tank" style={{ color: colors.text.link, fontSize: screens.xs ? '14px' : '16px' }}>Upload / Edit images</Link>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
|
<Card title="Emergency Deliveries" bordered={true} style={commonStyles.cardWarning}>
|
|
<Typography.Text style={{ fontSize: screens.xs ? '14px' : '16px', lineHeight: '1.6' }}>
|
|
We do deliveries after hours, nights, weekends, holidays as emergency. Snow may effect delivery. We reserve the right to cancel deliveries if tank is not safe to deliver.
|
|
</Typography.Text>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Row 3: My Deliveries (Full Width) */}
|
|
<Row gutter={screens.xs ? 8 : 16}>
|
|
<Col xs={24}>
|
|
<Card title="My Deliveries" bordered={false}>
|
|
{loadingDeliveries ? (
|
|
screens.xs && !screens.sm ? (
|
|
// Mobile: Skeleton Cards
|
|
<div style={{ maxHeight: '400px', overflowY: 'auto', overflowX: 'hidden' }}>
|
|
{[1, 2, 3].map((i) => (
|
|
<Card key={i} size="small" style={{ marginBottom: 8 }} bodyStyle={{ padding: '12px' }}>
|
|
<Row gutter={8} align="middle">
|
|
<Col xs={12}><Skeleton.Input active size="small" style={{ width: 80 }} /></Col>
|
|
<Col xs={12}><Skeleton.Input active size="small" style={{ width: 60 }} /></Col>
|
|
<Col xs={12}><Skeleton.Input active size="small" style={{ width: 70 }} /></Col>
|
|
<Col xs={12}><Skeleton.Input active size="small" style={{ width: 90 }} /></Col>
|
|
</Row>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
// Desktop/Tablet: Skeleton Table
|
|
<div style={{ overflowX: 'auto' }}>
|
|
<Skeleton active paragraph={{ rows: 5 }} />
|
|
</div>
|
|
)
|
|
) : deliveries.length > 0 ? (
|
|
screens.xs && !screens.sm ? (
|
|
// Mobile: Cards
|
|
<div style={{ maxHeight: '400px', overflowY: 'auto', overflowX: 'hidden' }}>
|
|
{deliveries.map((delivery) => {
|
|
let cardStyle = { marginBottom: spacing.xs, maxWidth: '100%' };
|
|
if (delivery.delivery_status === 'Waiting') {
|
|
cardStyle.backgroundColor = colors.successLight;
|
|
} else if (delivery.delivery_status === 'Out for Delivery') {
|
|
cardStyle.border = `2px solid ${colors.gold}`;
|
|
cardStyle.boxShadow = colors.goldGlow;
|
|
}
|
|
return (
|
|
<Card
|
|
key={delivery.id}
|
|
size="small"
|
|
style={cardStyle}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<Row gutter={8} align="middle">
|
|
<Col xs={12} sm={8}>
|
|
<strong style={{ fontSize: '12px' }}>Date:</strong><br />
|
|
<span style={{ fontSize: '12px', wordBreak: 'break-word' }}>{delivery.delivery_date ? new Date(delivery.delivery_date).toLocaleDateString() : 'N/A'}</span>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<strong style={{ fontSize: '12px' }}>Gallons:</strong><br />
|
|
<span style={{ fontSize: '12px' }}>{delivery.gallons ? `${delivery.gallons} gal` : 'N/A'}</span>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<strong style={{ fontSize: '12px' }}>Price:</strong><br />
|
|
<span style={{ fontSize: '12px' }}>{delivery.customer_price ? `$${delivery.customer_price.toFixed(2)}` : 'N/A'}</span>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<strong style={{ fontSize: '12px' }}>Status:</strong><br />
|
|
<span style={{ fontSize: '12px', wordBreak: 'break-word' }}>
|
|
{(() => {
|
|
if (delivery.delivery_status === 'Pending') return 'pending cc charge';
|
|
if (delivery.delivery_status === 'Finalized') return 'Delivered/Charged';
|
|
if (delivery.delivery_status === 'Waiting') return 'waiting for delivery';
|
|
return delivery.delivery_status;
|
|
})()}
|
|
</span>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<strong style={{ fontSize: '12px' }}>Payment:</strong><br />
|
|
<span style={{ fontSize: '12px', wordBreak: 'break-word' }}>{delivery.payment_type}</span>
|
|
</Col>
|
|
</Row>
|
|
</Card>);
|
|
})}
|
|
</div>
|
|
) : (
|
|
// Desktop/Tablet: Table
|
|
<div style={{ overflowX: 'auto', WebkitOverflowScrolling: 'touch' }}>
|
|
<Table
|
|
columns={deliveriesColumns}
|
|
dataSource={deliveries}
|
|
rowKey="id"
|
|
pagination={false}
|
|
size="small"
|
|
scroll={{ y: 400, x: 'max-content' }}
|
|
style={{ minWidth: '100%' }}
|
|
rowClassName={(record) => {
|
|
if (record.delivery_status === 'Waiting') return 'waiting-delivery';
|
|
if (record.delivery_status === 'Out for Delivery') return 'out-for-delivery';
|
|
return '';
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
) : (
|
|
<p style={{ fontSize: screens.xs ? '14px' : '16px' }}>No deliveries found.</p>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
)}
|
|
|
|
export default Index;
|