fixed image place holder, supported refunds, removed temp images

This commit is contained in:
2ManyProjects 2025-05-02 15:39:11 -05:00
parent 91b4c2de76
commit 0fe6a195ed
22 changed files with 663 additions and 99 deletions

View file

@ -105,12 +105,11 @@ module.exports = (pool, query, authMiddleware) => {
}
});
// Update order status (simple version)
// Update order status with refund process
router.patch('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const { status } = req.body;
const { status, refundReason } = req.body;
if (!req.user.is_admin) {
return res.status(403).json({
@ -128,25 +127,308 @@ module.exports = (pool, query, authMiddleware) => {
});
}
// Update order status
const result = await query(`
UPDATE orders
SET status = $1, updated_at = NOW()
WHERE id = $2
RETURNING *
`, [status, id]);
// Get order with customer information before updating
const orderResult = await query(`
SELECT o.*, o.payment_id, o.total_amount, u.email, u.first_name, u.last_name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = $1
`, [id]);
if (result.rows.length === 0) {
if (orderResult.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Order not found'
});
}
const order = orderResult.rows[0];
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// Special handling for refunded status
if (status === 'refunded' && order.status !== 'refunded') {
// Check if payment_id exists
if (!order.payment_id) {
await client.query('ROLLBACK');
return res.status(400).json({
error: true,
message: 'Cannot refund order without payment information'
});
}
try {
// Process the refund with Stripe
const stripe = require('stripe')(config.payment.stripeSecretKey);
// If the payment ID is a Stripe Checkout session ID, get the associated payment intent
let paymentIntentId = order.payment_id;
// If it starts with 'cs_', it's a checkout session
if (paymentIntentId.startsWith('cs_')) {
const session = await stripe.checkout.sessions.retrieve(paymentIntentId);
paymentIntentId = session.payment_intent;
}
// Create the refund
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
reason: 'requested_by_customer',
});
// Store refund information in the order
const refundInfo = {
refund_id: refund.id,
amount: refund.amount / 100, // Convert from cents to dollars
status: refund.status,
reason: refundReason || 'Customer request',
created: new Date(refund.created * 1000).toISOString(),
};
// Update order with refund information
await client.query(`
UPDATE orders
SET status = $1,
updated_at = NOW(),
payment_notes = CASE
WHEN payment_notes IS NULL THEN $2::jsonb
ELSE payment_notes::jsonb || $2::jsonb
END
WHERE id = $3
RETURNING *
`, [
status,
JSON.stringify({ refund: refundInfo }),
id
]);
// Send refund confirmation email
await emailService.sendRefundConfirmation({
to: order.email,
first_name: order.first_name || 'Customer',
order_id: order.id.substring(0, 8), // Use first 8 characters of UUID for cleaner display
refund_amount: `$${refundInfo.amount.toFixed(2)}`,
refund_date: new Date(refundInfo.created).toLocaleDateString(),
refund_method: 'Original payment method',
refund_reason: refundInfo.reason
});
} catch (stripeError) {
console.error('Stripe refund error:', stripeError);
await client.query('ROLLBACK');
return res.status(400).json({
error: true,
message: `Failed to process refund: ${stripeError.message}`
});
}
} else {
// For non-refund status updates, just update the status
await client.query(`
UPDATE orders
SET status = $1, updated_at = NOW()
WHERE id = $2
`, [status, id]);
}
await client.query('COMMIT');
// Get updated order
const updatedOrderResult = await query(
'SELECT * FROM orders WHERE id = $1',
[id]
);
res.json({
message: 'Order status updated successfully',
order: result.rows[0]
order: updatedOrderResult.rows[0]
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
router.post('/:id/refund', async (req, res, next) => {
try {
const { id } = req.params;
const {
amount,
reason,
refundItems, // Optional: array of { item_id, quantity } for partial refunds
sendEmail = true // Whether to send confirmation email
} = req.body;
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
message: 'Admin access required'
});
}
// Get order details with customer information
const orderResult = await query(`
SELECT o.*, o.payment_id, o.total_amount, u.email, u.first_name, u.last_name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = $1
`, [id]);
if (orderResult.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Order not found'
});
}
const order = orderResult.rows[0];
// Check if payment_id exists
if (!order.payment_id) {
return res.status(400).json({
error: true,
message: 'Cannot refund order without payment information'
});
}
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// Determine if this is a full or partial refund
const refundAmount = amount || order.total_amount;
const isFullRefund = Math.abs(refundAmount - order.total_amount) < 0.01; // Account for possible floating point issues
try {
// Process the refund with Stripe
const stripe = require('stripe')(config.payment.stripeSecretKey);
// If the payment ID is a Stripe Checkout session ID, get the associated payment intent
let paymentIntentId = order.payment_id;
// If it starts with 'cs_', it's a checkout session
if (paymentIntentId.startsWith('cs_')) {
const session = await stripe.checkout.sessions.retrieve(paymentIntentId);
paymentIntentId = session.payment_intent;
}
// Create the refund
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount: Math.round(refundAmount * 100), // Convert to cents for Stripe
reason: 'requested_by_customer',
});
// Store refund information in the order
const refundInfo = {
refund_id: refund.id,
amount: refund.amount / 100, // Convert from cents to dollars
status: refund.status,
reason: reason || 'Customer request',
created: new Date(refund.created * 1000).toISOString(),
is_full_refund: isFullRefund,
refunded_items: refundItems || []
};
// Update order status and refund info
if (isFullRefund) {
// Full refund changes order status to refunded
await client.query(`
UPDATE orders
SET status = 'refunded',
updated_at = NOW(),
payment_notes = CASE
WHEN payment_notes IS NULL THEN $1::jsonb
ELSE payment_notes::jsonb || $1::jsonb
END
WHERE id = $2
`, [
JSON.stringify({ refund: refundInfo }),
id
]);
} else {
// Partial refund doesn't change order status
await client.query(`
UPDATE orders
SET updated_at = NOW(),
payment_notes = CASE
WHEN payment_notes IS NULL THEN $1::jsonb
ELSE payment_notes::jsonb || $1::jsonb
END
WHERE id = $2
`, [
JSON.stringify({ partial_refund: refundInfo }),
id
]);
}
// If specific items were refunded, update their refund status in order_items
if (refundItems && refundItems.length > 0) {
for (const item of refundItems) {
await client.query(`
UPDATE order_items
SET refunded = true, refunded_quantity = $1
WHERE id = $2 AND order_id = $3
`, [
item.quantity,
item.item_id,
id
]);
}
}
// Send refund confirmation email if requested
if (sendEmail) {
await emailService.sendRefundConfirmation({
to: order.email,
first_name: order.first_name || 'Customer',
order_id: order.id.substring(0, 8), // Use first 8 characters of UUID for cleaner display
refund_amount: `$${refundInfo.amount.toFixed(2)}`,
refund_date: new Date(refundInfo.created).toLocaleDateString(),
refund_method: 'Original payment method',
refund_reason: refundInfo.reason
});
}
await client.query('COMMIT');
// Get the updated order
const updatedOrderResult = await query(
'SELECT * FROM orders WHERE id = $1',
[id]
);
res.json({
success: true,
message: `${isFullRefund ? 'Full' : 'Partial'} refund processed successfully`,
refund: refundInfo,
order: updatedOrderResult.rows[0]
});
} catch (stripeError) {
console.error('Stripe refund error:', stripeError);
await client.query('ROLLBACK');
return res.status(400).json({
error: true,
message: `Failed to process refund: ${stripeError.message}`
});
}
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}

View file

@ -604,6 +604,26 @@ const emailService = {
// Don't throw error, just log it
return null;
}
},
/**
* Send a refund confirmation email
* @param {Object} options - Options
* @param {string} options.to - Recipient email
* @param {string} options.first_name - Customer's first name
* @param {string} options.order_id - Order ID
* @param {string} options.refund_amount - Refund amount
* @param {string} options.refund_date - Refund date
* @param {string} options.refund_method - Refund method (e.g., "Original payment method")
* @param {string} options.refund_reason - Reason for refund
* @returns {Promise<boolean>} Success status
*/
async sendRefundConfirmation(options) {
return this.sendTemplatedEmail({
to: options.to,
templateType: 'refund_confirmation',
variables: options
});
}
};

View file

@ -24,7 +24,7 @@ SELECT
'Purple',
'Quartz',
'Brazil',
'/images/amethyst-geode.jpg'
NULL
FROM categories WHERE name = 'Rock';
WITH categories AS (
@ -41,7 +41,7 @@ SELECT
'Gray/Blue',
'Feldspar',
'Madagascar',
'/images/labradorite.jpg'
NULL
FROM categories WHERE name = 'Rock';
WITH categories AS (
@ -58,7 +58,7 @@ SELECT
'Turquoise',
'Turquoise',
'Arizona',
'/images/turquoise.jpg'
NULL
FROM categories WHERE name = 'Rock';
-- Insert bone products
@ -74,7 +74,7 @@ SELECT
8,
38.5,
'Antler',
'/images/deer-antler.jpg'
NULL
FROM categories WHERE name = 'Bone';
WITH categories AS (
@ -89,7 +89,7 @@ SELECT
5,
22.8,
'Fossilized Bone',
'/images/fossil-fish.jpg'
NULL
FROM categories WHERE name = 'Bone';
-- Insert stick products
@ -107,7 +107,7 @@ SELECT
8.3,
'Driftwood',
'Tan/Gray',
'/images/driftwood.jpg'
NULL
FROM categories WHERE name = 'Stick';
WITH categories AS (
@ -124,7 +124,7 @@ SELECT
3.8,
'Maple',
'Brown',
'/images/walking-stick.jpg'
NULL
FROM categories WHERE name = 'Stick';
WITH categories AS (
@ -141,7 +141,7 @@ SELECT
1.5,
'Birch',
'White',
'/images/birch-branches.jpg'
NULL
FROM categories WHERE name = 'Stick';
-- Create a cart for testing

View file

@ -14,31 +14,3 @@ CREATE INDEX idx_product_images_product_id ON product_images(product_id);
-- Modify existing products table to remove single image_url field
ALTER TABLE products DROP COLUMN IF EXISTS image_url;
-- Insert test images for existing products
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/amethyst-geode.jpg', 0, true FROM products WHERE name = 'Amethyst Geode';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/amethyst-geode-closeup.jpg', 1, false FROM products WHERE name = 'Amethyst Geode';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/labradorite.jpg', 0, true FROM products WHERE name = 'Polished Labradorite';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/turquoise.jpg', 0, true FROM products WHERE name = 'Raw Turquoise';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/deer-antler.jpg', 0, true FROM products WHERE name = 'Deer Antler';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/fossil-fish.jpg', 0, true FROM products WHERE name = 'Fossil Fish';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/driftwood.jpg', 0, true FROM products WHERE name = 'Driftwood Piece';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/walking-stick.jpg', 0, true FROM products WHERE name = 'Walking Stick';
INSERT INTO product_images (product_id, image_path, display_order, is_primary)
SELECT id, '/images/birch-branches.jpg', 0, true FROM products WHERE name = 'Decorative Branch Set';

View file

@ -51,3 +51,11 @@ VALUES (
'email_templates'
)
ON CONFLICT (key) DO NOTHING;
INSERT INTO system_settings (key, value, category)
VALUES (
'email_template_refund_confirmation_default',
'{"name":"Refund Confirmation Template","type":"refund_confirmation","subject":"Your Refund for Order #{{order_id}} Has Been Processed","content":"<div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;\"><div style=\"background-color: #f8f8f8; padding: 20px; text-align: center;\"><h1 style=\"color: #333;\">Refund Confirmation</h1><p style=\"font-size: 16px;\">Order #{{order_id}}</p></div><div style=\"padding: 20px;\"><p>Hello {{first_name}},</p><p>We are writing to confirm that your refund for order #{{order_id}} has been processed. The refund amount of {{refund_amount}} has been issued to your original payment method.</p><div style=\"background-color: #f8f8f8; padding: 15px; margin: 20px 0;\"><h3 style=\"margin-top: 0;\">Refund Details</h3><p><strong>Refund Amount:</strong> {{refund_amount}}</p><p><strong>Refund Date:</strong> {{refund_date}}</p><p><strong>Refund Method:</strong> {{refund_method}}</p><p><strong>Refund Reason:</strong> {{refund_reason}}</p></div><p>Please note that it may take 5-10 business days for the refund to appear in your account, depending on your payment provider.</p><p>If you have any questions about this refund, please do not hesitate to contact our customer support team.</p><p>Thank you for your understanding.</p></div><div style=\"background-color: #333; color: white; padding: 15px; text-align: center; font-size: 12px;\"><p>&copy; 2025 Rocks, Bones & Sticks. All rights reserved.</p></div></div>","isDefault":true,"createdAt":"2025-05-02T00:00:00.000Z"}',
'email_templates'
)
ON CONFLICT (key) DO NOTHING;

View file

@ -0,0 +1,4 @@
-- Add refund-related columns to order_items table
ALTER TABLE order_items ADD COLUMN IF NOT EXISTS refunded BOOLEAN DEFAULT FALSE;
ALTER TABLE order_items ADD COLUMN IF NOT EXISTS refunded_quantity INTEGER;
ALTER TABLE order_items ADD COLUMN IF NOT EXISTS refund_reason TEXT;

View file

@ -30,7 +30,7 @@ const Footer = ({brandingSettings}) => {
<Container maxWidth="lg">
<Grid container spacing={3}>
<Grid item xs={12} sm={4}>
{logoUrl ? (
{brandingSettings?.logo_url ? (
<Box
component="img"
src={logoUrl}

View file

@ -14,9 +14,10 @@ import {
Grid,
Typography,
Divider,
Box
Box,
Alert
} from '@mui/material';
import { useUpdateOrderStatus } from '@hooks/adminHooks';
import { useUpdateOrderStatus, useRefundOrder } from '@hooks/adminHooks';
// List of supported shipping carriers
const SHIPPING_CARRIERS = [
@ -29,6 +30,18 @@ const SHIPPING_CARRIERS = [
{ value: 'other', label: 'Other (specify)' }
];
// List of refund reasons
const REFUND_REASONS = [
{ value: 'customer_request', label: 'Customer Request' },
{ value: 'item_damaged', label: 'Item Arrived Damaged' },
{ value: 'item_missing', label: 'Items Missing from Order' },
{ value: 'wrong_item', label: 'Wrong Item Received' },
{ value: 'quality_issue', label: 'Quality Not as Expected' },
{ value: 'late_delivery', label: 'Excessive Shipping Delay' },
{ value: 'order_mistake', label: 'Mistake in Order Processing' },
{ value: 'other', label: 'Other' }
];
const OrderStatusDialog = ({ open, onClose, order }) => {
const [status, setStatus] = useState(order ? order.status : 'pending');
const [shippingData, setShippingData] = useState({
@ -39,8 +52,14 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
estimatedDelivery: '',
customerMessage: ''
});
const [refundData, setRefundData] = useState({
reason: 'customer_request',
customReason: '',
sendEmail: true
});
const updateOrderStatus = useUpdateOrderStatus();
const refundOrder = useRefundOrder();
const handleStatusChange = (e) => {
setStatus(e.target.value);
@ -54,6 +73,22 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
}));
};
const handleRefundDataChange = (e) => {
const { name, value } = e.target;
setRefundData(prev => ({
...prev,
[name]: value
}));
};
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setRefundData(prev => ({
...prev,
[name]: checked
}));
};
const handleSave = () => {
// For shipped status, require tracking number
if (status === 'shipped' && !shippingData.trackingNumber) {
@ -67,10 +102,28 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
shipperValue = shippingData.otherShipper;
}
// For refunded status, use the refund mutation
if (status === 'refunded') {
// Determine the actual reason value to send
let reasonValue = refundData.reason;
if (reasonValue === 'other' && refundData.customReason) {
reasonValue = refundData.customReason;
}
refundOrder.mutate({
orderId: order.id,
reason: reasonValue,
sendEmail: refundData.sendEmail
}, {
onSuccess: () => {
onClose();
}
});
} else if (status === 'shipped') {
// For shipped status, include shipping information
updateOrderStatus.mutate({
orderId: order.id,
status,
...(status === 'shipped' && {
shippingData: {
...shippingData,
shipper: shipperValue,
@ -78,18 +131,37 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
shippedDate: new Date().toISOString().split('T')[0]
},
sendNotification: true
})
}, {
onSuccess: () => {
onClose();
}
});
} else {
// For other statuses, just update the status
updateOrderStatus.mutate({
orderId: order.id,
status
}, {
onSuccess: () => {
onClose();
}
});
}
};
const isPending = updateOrderStatus.isLoading || refundOrder.isLoading;
const error = updateOrderStatus.error || refundOrder.error;
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>Update Order Status</DialogTitle>
<DialogContent dividers>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error.message || 'An error occurred while updating the order.'}
</Alert>
)}
<Grid container spacing={3}>
<Grid item xs={12}>
<FormControl fullWidth>
@ -205,6 +277,70 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
</Grid>
</>
)}
{status === 'refunded' && (
<>
<Grid item xs={12}>
<Divider sx={{ my: 1 }} />
<Typography variant="subtitle1" gutterBottom>
Refund Information
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
The refund will be processed through Stripe to the customer's original payment method.
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel id="refund-reason-label">Refund Reason</InputLabel>
<Select
labelId="refund-reason-label"
name="reason"
value={refundData.reason}
label="Refund Reason"
onChange={handleRefundDataChange}
>
{REFUND_REASONS.map((reason) => (
<MenuItem key={reason.value} value={reason.value}>
{reason.label}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
{refundData.reason === 'other' && (
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Specify Reason"
name="customReason"
value={refundData.customReason}
onChange={handleRefundDataChange}
placeholder="Enter specific reason for refund"
/>
</Grid>
)}
<Grid item xs={12}>
<FormControl fullWidth>
<div style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
id="sendEmail"
name="sendEmail"
checked={refundData.sendEmail}
onChange={(e) => handleCheckboxChange(e)}
style={{ marginRight: '8px' }}
/>
<label htmlFor="sendEmail">
Send refund confirmation email to customer
</label>
</div>
</FormControl>
</Grid>
</>
)}
</Grid>
</DialogContent>
<DialogActions>
@ -213,9 +349,9 @@ const OrderStatusDialog = ({ open, onClose, order }) => {
onClick={handleSave}
variant="contained"
color="primary"
disabled={updateOrderStatus.isLoading}
disabled={isPending}
>
{updateOrderStatus.isLoading ? <CircularProgress size={24} /> : 'Save'}
{isPending ? <CircularProgress size={24} /> : 'Save'}
</Button>
</DialogActions>
</Dialog>

View file

@ -15,7 +15,7 @@ const ProductImage = ({
images,
alt = 'Product image',
sx = {},
placeholderImage = '/placeholder.jpg',
placeholderImage = "https://placehold.co/600x400/000000/FFFF",
...rest
}) => {
const [imageError, setImageError] = useState(false);

View file

@ -130,3 +130,34 @@ export const useUpdateOrderStatus = () => {
}
});
};
/**
* Hook for processing a refund (admin only)
*/
export const useRefundOrder = () => {
const queryClient = useQueryClient();
const notification = useNotification();
return useMutation({
mutationFn: ({ orderId, reason, sendEmail, amount, refundItems }) => {
return adminOrderService.processRefund(
orderId,
reason,
sendEmail,
amount,
refundItems
);
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['admin-orders'] });
queryClient.invalidateQueries({ queryKey: ['admin-order', data.order.id] });
notification.showNotification('Refund processed successfully', 'success');
},
onError: (error) => {
notification.showNotification(
error.message || 'Failed to process refund',
'error'
);
}
});
};

View file

@ -44,11 +44,8 @@ const AdminLayout = () => {
const { data: brandingSettings } = useBrandingSettings();
// Get site name from branding settings or use default
const siteName = brandingSettings?.site_name || 'Rocks, Bones & Sticks';
// Get logo URL from branding settings
const logoUrl = imageUtils.getImageUrl(brandingSettings?.logo_url)
// Force drawer closed on mobile
useEffect(() => {

View file

@ -45,7 +45,7 @@ const AuthLayout = () => {
mb: 4,
}}
>
{logoUrl ? (
{brandingSettings?.logo_url ? (
<Box
component="img"
src={logoUrl}

View file

@ -79,7 +79,7 @@ const MainLayout = () => {
const drawer = (
<Box sx={{ width: 250 }} role="presentation" onClick={handleDrawerToggle}>
<Box sx={{ display: 'flex', p: 2, alignItems: 'center' }}>
{logoUrl ? (
{brandingSettings?.logo_url ? (
<Box
component="img"
src={logoUrl}
@ -157,7 +157,7 @@ const MainLayout = () => {
<MenuIcon />
</IconButton>
{logoUrl ? (
{brandingSettings?.logo_url ? (
<Box
component={RouterLink}
to="/"

View file

@ -591,7 +591,100 @@ const AdminOrdersPage = () => {
</Grid>
)}
{/* Refund Information - Only show for refunded orders */}
{orderDetails && orderDetails.status === 'refunded' && orderDetails.payment_notes && (
<Grid item xs={12} sx={{ mt: 2 }}>
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom>
Refund Information
</Typography>
<Divider sx={{ mb: 2 }} />
{(() => {
// Parse refund info from JSON if needed
let refundInfo = null;
if (typeof orderDetails.payment_notes === 'string') {
try {
const paymentNotes = JSON.parse(orderDetails.payment_notes);
refundInfo = paymentNotes.refund;
} catch (e) {
console.error('Failed to parse payment notes:', e);
}
} else if (orderDetails.payment_notes && orderDetails.payment_notes.refund) {
refundInfo = orderDetails.payment_notes.refund;
}
if (!refundInfo) {
return (
<Typography color="text.secondary">
No detailed refund information available.
</Typography>
);
}
return (
<Box>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
Refund Amount:
</Typography>
<Typography variant="body1">
${parseFloat(refundInfo.amount).toFixed(2)}
</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
Refund Date:
</Typography>
<Typography variant="body1">
{formatDate(refundInfo.created)}
</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
Refund Status:
</Typography>
<Typography variant="body1">
<Chip
label={refundInfo.status || 'processed'}
color="success"
size="small"
/>
</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
Refund Reason:
</Typography>
<Typography variant="body1">
{refundInfo.reason || 'Not specified'}
</Typography>
</Grid>
{refundInfo.refund_id && (
<Grid item xs={12} sm={6}>
<Typography variant="subtitle2" color="text.secondary">
Refund ID:
</Typography>
<Typography variant="body1" sx={{ fontFamily: 'monospace' }}>
{refundInfo.refund_id}
</Typography>
</Grid>
)}
</Grid>
</Box>
);
})()}
</CardContent>
</Card>
</Grid>
)}
{/* Order Items */}
<Grid item xs={12}>
<Card variant="outlined">

View file

@ -227,10 +227,9 @@ const BlogPage = () => {
height="200"
image={post.featured_image_path
? imageUtils.getImageUrl(post.featured_image_path)
: '/images/placeholder.jpg'}
: "https://placehold.co/600x400/000000/FFFF"}
alt={post.title}
/>
<CardContent sx={{ flexGrow: 1 }}>
{/* Category */}
{post.category_name && (

View file

@ -162,7 +162,7 @@ const CartPage = () => {
<Card sx={{ height: '100%' }}>
<CardMedia
component="img"
image={imageUtils.getImageUrl(item.primary_image.path || '/images/placeholder.jpg')}
image={item.primary_image.path ? imageUtils.getImageUrl(item.primary_image.path) : "https://placehold.co/600x400/000000/FFFF"}
alt={item.name}
sx={{ height: 80, objectFit: 'cover' }}
/>

View file

@ -20,7 +20,7 @@ const HomePage = () => {
py: 8,
mb: 6,
borderRadius: 2,
backgroundImage: 'linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(/images/hero-background.jpg)',
backgroundImage: 'linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5))',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
@ -72,7 +72,7 @@ const HomePage = () => {
<CardMedia
component="img"
height="200"
image={imageUtils.getImageUrl(category.image_path)}
image={category.image_path ? imageUtils.getImageUrl(category.image_path) : "https://placehold.co/600x400/000000/FFFF"}
alt={category.name}
/>
<CardContent>
@ -115,9 +115,9 @@ const HomePage = () => {
<CardMedia
component="img"
height="200"
image={imageUtils.getImageUrl((product.images && product.images.length > 0)
? product.images.find(img => img.isPrimary)?.path || product.images[0].path
: '/images/placeholder.jpg')}
image={(product.images && product.images.length > 0)
? imageUtils.getImageUrl(product.images.find(img => img.isPrimary)?.path || product.images[0].path
) : "https://placehold.co/600x400/000000/FFFF"}
alt={product.name}
/>
<CardContent sx={{ flexGrow: 1 }}>

View file

@ -204,10 +204,9 @@ const ProductDetailPage = () => {
<Card>
<CardMedia
component="img"
image={
imageUtils.getImageUrl(product.images && product.images.length > 0
? product.images[selectedImage]?.path
: '/images/placeholder.jpg')
image={product.images && product.images.length > 0
?
imageUtils.getImageUrl(product.images[selectedImage]?.path) : "https://placehold.co/600x400/000000/FFFF"
}
alt={product.name}
sx={{

View file

@ -343,9 +343,9 @@ const ProductsPage = () => {
<CardMedia
component="img"
height="200"
image={imageUtils.getImageUrl((product.images && product.images.length > 0)
? product.images.find(img => img.isPrimary)?.path || product.images[0].path
: '/images/placeholder.jpg')}
image={(product.images && product.images.length > 0)
? imageUtils.getImageUrl( product.images.find(img => img.isPrimary)?.path || product.images[0].path
) : "https://placehold.co/600x400/000000/FFFF"}
alt={product.name}
sx={{ objectFit: 'cover' }}
onClick={() => navigate(`/products/${product.id}`)}

View file

@ -123,5 +123,28 @@ export const adminOrderService = {
} catch (error) {
throw error.response?.data || { message: 'An unknown error occurred' };
}
},
/**
* Process a refund for an order (admin only)
* @param {string} id - Order ID
* @param {string} reason - Refund reason
* @param {boolean} [sendEmail=true] - Whether to send a confirmation email
* @param {number} [amount] - Optional amount for partial refunds
* @param {Array} [refundItems] - Optional array of items to refund
* @returns {Promise} Promise with the API response
*/
processRefund: async (id, reason, sendEmail = true, amount = null, refundItems = null) => {
try {
const response = await apiClient.post(`/admin/orders/${id}/refund`, {
reason,
sendEmail,
...(amount && { amount }),
...(refundItems && { refundItems })
});
return response.data;
} catch (error) {
throw error.response?.data || { message: 'An unknown error occurred' };
}
}
};

View file

@ -66,7 +66,7 @@ export const imageService = {
* @returns {string} The full image URL
*/
getImageUrl: (imagePath) => {
if (!imagePath) return '/images/placeholder.jpg';
if (!imagePath) return "https://placehold.co/600x400/000000/FFFF";
// If it's already a full URL, return it
if (imagePath.startsWith('http')) return imagePath;

View file

@ -10,7 +10,7 @@ const imageUtils = {
* @param {string} [defaultImage] - Default image to use if the path is invalid
* @returns {string} - The full image URL
*/
getImageUrl: (imagePath, defaultImage = '/placeholder.jpg') => {
getImageUrl: (imagePath, defaultImage = "https://placehold.co/600x400/000000/FFFF") => {
if (!imagePath) return defaultImage;
// If it's already a complete URL, return it as is