diff --git a/backend/src/routes/orderAdmin.js b/backend/src/routes/orderAdmin.js index 9bd5b2d..2180dcb 100644 --- a/backend/src/routes/orderAdmin.js +++ b/backend/src/routes/orderAdmin.js @@ -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,30 +127,313 @@ 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' }); } - res.json({ - message: 'Order status updated successfully', - order: result.rows[0] - }); + 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: 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); + } + }); + // Update order status with shipping information and send notification router.patch('/:id/shipping', async (req, res, next) => { try { diff --git a/backend/src/services/emailService.js b/backend/src/services/emailService.js index c07381a..093ad00 100644 --- a/backend/src/services/emailService.js +++ b/backend/src/services/emailService.js @@ -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} Success status + */ + async sendRefundConfirmation(options) { + return this.sendTemplatedEmail({ + to: options.to, + templateType: 'refund_confirmation', + variables: options + }); } }; diff --git a/db/init/02-seed.sql b/db/init/02-seed.sql index a39a2c3..f14eaa9 100644 --- a/db/init/02-seed.sql +++ b/db/init/02-seed.sql @@ -24,7 +24,7 @@ SELECT 'Purple', 'Quartz', 'Brazil', - '/images/amethyst-geode.jpg' + NULL FROM categories WHERE name = 'Rock'; WITH categories AS ( @@ -40,8 +40,8 @@ SELECT 120.5, 'Gray/Blue', 'Feldspar', - 'Madagascar', - '/images/labradorite.jpg' + 'Madagascar', + NULL FROM categories WHERE name = 'Rock'; WITH categories AS ( @@ -57,8 +57,8 @@ SELECT 85.2, 'Turquoise', 'Turquoise', - 'Arizona', - '/images/turquoise.jpg' + 'Arizona', + NULL FROM categories WHERE name = 'Rock'; -- Insert bone products @@ -73,8 +73,8 @@ SELECT 24.99, 8, 38.5, - 'Antler', - '/images/deer-antler.jpg' + 'Antler', + NULL FROM categories WHERE name = 'Bone'; WITH categories AS ( @@ -88,8 +88,8 @@ SELECT 89.99, 5, 22.8, - 'Fossilized Bone', - '/images/fossil-fish.jpg' + 'Fossilized Bone', + NULL FROM categories WHERE name = 'Bone'; -- Insert stick products @@ -106,8 +106,8 @@ SELECT 45.6, 8.3, 'Driftwood', - 'Tan/Gray', - '/images/driftwood.jpg' + 'Tan/Gray', + NULL FROM categories WHERE name = 'Stick'; WITH categories AS ( @@ -123,8 +123,8 @@ SELECT 152.4, 3.8, 'Maple', - 'Brown', - '/images/walking-stick.jpg' + 'Brown', + NULL FROM categories WHERE name = 'Stick'; WITH categories AS ( @@ -140,8 +140,8 @@ SELECT 76.2, 1.5, 'Birch', - 'White', - '/images/birch-branches.jpg' + 'White', + NULL FROM categories WHERE name = 'Stick'; -- Create a cart for testing diff --git a/db/init/04-product-images.sql b/db/init/04-product-images.sql index 47a626f..54b71a1 100644 --- a/db/init/04-product-images.sql +++ b/db/init/04-product-images.sql @@ -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'; \ No newline at end of file diff --git a/db/init/18-email-templates.sql b/db/init/18-email-templates.sql index 0c88053..e7b25e5 100644 --- a/db/init/18-email-templates.sql +++ b/db/init/18-email-templates.sql @@ -50,4 +50,12 @@ VALUES ( '{"name":"Welcome Email Template","type":"welcome_email","subject":"Welcome to Rocks, Bones & Sticks!","content":"

Welcome to Rocks, Bones & Sticks!

Hello {{first_name}},

Thank you for creating an account with us. We are excited to have you join our community of natural curiosity enthusiasts!

As a member, you will enjoy:

  • Access to our unique collection of natural specimens
  • Special offers and promotions
  • Early access to new items

Start exploring our collections today and discover the beauty of nature!

Shop Now

© 2025 Rocks, Bones & Sticks. All rights reserved.

","isDefault":true,"createdAt":"2025-04-29T00:00:00.000Z"}', '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":"

Refund Confirmation

Order #{{order_id}}

Hello {{first_name}},

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.

Refund Details

Refund Amount: {{refund_amount}}

Refund Date: {{refund_date}}

Refund Method: {{refund_method}}

Refund Reason: {{refund_reason}}

Please note that it may take 5-10 business days for the refund to appear in your account, depending on your payment provider.

If you have any questions about this refund, please do not hesitate to contact our customer support team.

Thank you for your understanding.

© 2025 Rocks, Bones & Sticks. All rights reserved.

","isDefault":true,"createdAt":"2025-05-02T00:00:00.000Z"}', + 'email_templates' +) ON CONFLICT (key) DO NOTHING; \ No newline at end of file diff --git a/db/init/21-order-refund.sql b/db/init/21-order-refund.sql new file mode 100644 index 0000000..807cd5b --- /dev/null +++ b/db/init/21-order-refund.sql @@ -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; \ No newline at end of file diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index 94009c8..5045d4b 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -30,7 +30,7 @@ const Footer = ({brandingSettings}) => { - {logoUrl ? ( + {brandingSettings?.logo_url ? ( { 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); @@ -53,6 +72,22 @@ const OrderStatusDialog = ({ open, onClose, order }) => { [name]: value })); }; + + 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 @@ -66,11 +101,29 @@ const OrderStatusDialog = ({ open, onClose, order }) => { if (shipperValue === 'other' && shippingData.otherShipper) { shipperValue = shippingData.otherShipper; } - - updateOrderStatus.mutate({ - orderId: order.id, - status, - ...(status === 'shipped' && { + + // 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, shippingData: { ...shippingData, shipper: shipperValue, @@ -78,18 +131,37 @@ const OrderStatusDialog = ({ open, onClose, order }) => { shippedDate: new Date().toISOString().split('T')[0] }, sendNotification: true - }) - }, { - onSuccess: () => { - onClose(); - } - }); + }, { + 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 ( Update Order Status + {error && ( + + {error.message || 'An error occurred while updating the order.'} + + )} + @@ -205,6 +277,70 @@ const OrderStatusDialog = ({ open, onClose, order }) => { )} + + {status === 'refunded' && ( + <> + + + + Refund Information + + + The refund will be processed through Stripe to the customer's original payment method. + + + + + + Refund Reason + + + + + {refundData.reason === 'other' && ( + + + + )} + + + +
+ handleCheckboxChange(e)} + style={{ marginRight: '8px' }} + /> + +
+
+
+ + )}
@@ -213,9 +349,9 @@ const OrderStatusDialog = ({ open, onClose, order }) => { onClick={handleSave} variant="contained" color="primary" - disabled={updateOrderStatus.isLoading} + disabled={isPending} > - {updateOrderStatus.isLoading ? : 'Save'} + {isPending ? : 'Save'}
diff --git a/frontend/src/components/ProductImage.jsx b/frontend/src/components/ProductImage.jsx index 8a7a411..87e1aea 100644 --- a/frontend/src/components/ProductImage.jsx +++ b/frontend/src/components/ProductImage.jsx @@ -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); diff --git a/frontend/src/hooks/adminHooks.js b/frontend/src/hooks/adminHooks.js index 088813f..f618546 100644 --- a/frontend/src/hooks/adminHooks.js +++ b/frontend/src/hooks/adminHooks.js @@ -129,4 +129,35 @@ 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' + ); + } + }); }; \ No newline at end of file diff --git a/frontend/src/layouts/AdminLayout.jsx b/frontend/src/layouts/AdminLayout.jsx index 43fa0fc..cb4b385 100644 --- a/frontend/src/layouts/AdminLayout.jsx +++ b/frontend/src/layouts/AdminLayout.jsx @@ -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(() => { diff --git a/frontend/src/layouts/AuthLayout.jsx b/frontend/src/layouts/AuthLayout.jsx index d6647da..d11de98 100644 --- a/frontend/src/layouts/AuthLayout.jsx +++ b/frontend/src/layouts/AuthLayout.jsx @@ -45,7 +45,7 @@ const AuthLayout = () => { mb: 4, }} > - {logoUrl ? ( + {brandingSettings?.logo_url ? ( { const drawer = ( - {logoUrl ? ( + {brandingSettings?.logo_url ? ( { - {logoUrl ? ( + {brandingSettings?.logo_url ? ( {
)} - + {/* Refund Information - Only show for refunded orders */} + {orderDetails && orderDetails.status === 'refunded' && orderDetails.payment_notes && ( + + + + + Refund Information + + + + {(() => { + // 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 ( + + No detailed refund information available. + + ); + } + + return ( + + + + + Refund Amount: + + + ${parseFloat(refundInfo.amount).toFixed(2)} + + + + + + Refund Date: + + + {formatDate(refundInfo.created)} + + + + + + Refund Status: + + + + + + + + + Refund Reason: + + + {refundInfo.reason || 'Not specified'} + + + + {refundInfo.refund_id && ( + + + Refund ID: + + + {refundInfo.refund_id} + + + )} + + + ); + })()} + + + + )} {/* Order Items */} diff --git a/frontend/src/pages/BlogPage.jsx b/frontend/src/pages/BlogPage.jsx index d11bf5d..3d8dd0c 100644 --- a/frontend/src/pages/BlogPage.jsx +++ b/frontend/src/pages/BlogPage.jsx @@ -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} /> - {/* Category */} {post.category_name && ( diff --git a/frontend/src/pages/CartPage.jsx b/frontend/src/pages/CartPage.jsx index 39ba2bf..388e55d 100644 --- a/frontend/src/pages/CartPage.jsx +++ b/frontend/src/pages/CartPage.jsx @@ -162,7 +162,7 @@ const CartPage = () => { diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx index d259c66..973a080 100644 --- a/frontend/src/pages/HomePage.jsx +++ b/frontend/src/pages/HomePage.jsx @@ -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 = () => { @@ -115,9 +115,9 @@ const HomePage = () => { 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} /> diff --git a/frontend/src/pages/ProductDetailPage.jsx b/frontend/src/pages/ProductDetailPage.jsx index 2784e47..fdeff12 100644 --- a/frontend/src/pages/ProductDetailPage.jsx +++ b/frontend/src/pages/ProductDetailPage.jsx @@ -204,10 +204,9 @@ const ProductDetailPage = () => { 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={{ diff --git a/frontend/src/pages/ProductsPage.jsx b/frontend/src/pages/ProductsPage.jsx index 5003cc1..2f70c60 100644 --- a/frontend/src/pages/ProductsPage.jsx +++ b/frontend/src/pages/ProductsPage.jsx @@ -343,9 +343,9 @@ const ProductsPage = () => { 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}`)} diff --git a/frontend/src/services/adminService.js b/frontend/src/services/adminService.js index 74843d6..bfc980a 100644 --- a/frontend/src/services/adminService.js +++ b/frontend/src/services/adminService.js @@ -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' }; + } } }; \ No newline at end of file diff --git a/frontend/src/services/imageService.js b/frontend/src/services/imageService.js index cb881dc..913e593 100644 --- a/frontend/src/services/imageService.js +++ b/frontend/src/services/imageService.js @@ -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; diff --git a/frontend/src/utils/imageUtils.js b/frontend/src/utils/imageUtils.js index a359f91..bd0d227 100644 --- a/frontend/src/utils/imageUtils.js +++ b/frontend/src/utils/imageUtils.js @@ -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