Files
frontend/src/pages/Index.jsx
2026-01-17 15:16:36 -05:00

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;