Okay, this is a good breakdown of the order return flow. Let's create a data structure that captures this information on the client-side using TypeScript and useReducer
with Context. This approach offers good state management and allows for persistence.
Data Structure (TypeScript)
interface ProductDetails {
productId: string;
salesLineId: string;
product: {
name: string;
manufacturer: string;
productUrl: string;
titleImageId: string;
};
price: {
amount: number;
currency: string;
};
returnableQuantity: number | null; // You might want to calculate this
returnableUntil: string | null;
returnable: boolean;
unreturnableReason: string | null;
returnSubItems: ProductDetails[]; // Assuming sub-items follow the same structure
}
interface ReturnableProduct extends ProductDetails {
isSelected: boolean;
quantityToReturn: number;
reason?: string;
returnMethod?: 'POST' | 'DROP_OFF'; // Assuming these are the possible methods
availableReturnMethods?: ('POST' | 'DROP_OFF')[]; // If available methods are dynamic
}
interface ReturnState {
orderId: string; // Or salesReference
outletId: string;
originalProducts: ProductDetails[]; // Keep the original data
returnableProducts: ReturnableProduct[]; // Products the user can return
selectedProducts: ReturnableProduct[]; // Products the user has selected for return
currentStep: 'PRODUCT_SELECTION' | 'REASON_SELECTION' | 'METHOD_SELECTION' | 'CONFIRMATION';
returnReasons: string[]; // Predefined reasons
}
// Initial state
const initialReturnState: ReturnState = {
orderId: '',
outletId: '',
originalProducts: [],
returnableProducts: [],
selectedProducts: [],
currentStep: 'PRODUCT_SELECTION',
returnReasons: ['Too small', 'Too large', 'Damaged', 'Not as described', 'Changed my mind'], // Example reasons
};
Explanation:
ProductDetails
: This mirrors the structure of the products received from your API.ReturnableProduct
: This extendsProductDetails
and adds the specific data we need for the return process:isSelected
: Whether the user has selected this product for return.quantityToReturn
: The number of items of this product the user wants to return.reason
: The reason selected for returning this product.returnMethod
: The chosen method for returning this product.availableReturnMethods
: If the available return methods are dynamic (not always 'POST' and 'DROP_OFF'), store them here.
ReturnState
: This is the main state object managed by the reducer:orderId
: Identifies the order.outletId
: Identifies the outlet.originalProducts
: Stores the original product data fetched from the API. This is useful for reference.returnableProducts
: A processed list of products that are potentially returnable, enhanced with selection and quantity information.selectedProducts
: An array ofReturnableProduct
objects that the user has explicitly chosen to return.currentStep
: Tracks the current stage of the return process.returnReasons
: An array of predefined reasons.
Reducer Actions (TypeScript)
type ReturnAction =
| { type: 'SET_ORDER_OPTIONS'; payload: any } // Payload will be your API response
| { type: 'TOGGLE_PRODUCT_SELECTION'; payload: { productId: string } }
| { type: 'SET_QUANTITY_TO_RETURN'; payload: { productId: string; quantity: number } }
| { type: 'SET_REASON'; payload: { productId: string; reason: string } }
| { type: 'SET_RETURN_METHOD'; payload: { productId: string; method: 'POST' | 'DROP_OFF' } }
| { type: 'NEXT_STEP' }
| { type: 'PREVIOUS_STEP' };
const returnReducer = (state: ReturnState, action: ReturnAction): ReturnState => {
switch (action.type) {
case 'SET_ORDER_OPTIONS': {
const { salesReference, outletId, products } = action.payload.data.returnOrderOptionsV2;
const returnableProducts: ReturnableProduct[] = products.map(
(product: ProductDetails) => ({
...product,
isSelected: false,
quantityToReturn: 0,
})
);
return {
...state,
orderId: salesReference,
outletId,
originalProducts: products,
returnableProducts,
};
}
case 'TOGGLE_PRODUCT_SELECTION': {
const { productId } = action.payload;
return {
...state,
returnableProducts: state.returnableProducts.map((product) =>
product.productId === productId ? { ...product, isSelected: !product.isSelected, quantityToReturn: product.isSelected ? 0 : 1 } : product
),
};
}
case 'SET_QUANTITY_TO_RETURN': {
const { productId, quantity } = action.payload;
return {
...state,
returnableProducts: state.returnableProducts.map((product) =>
product.productId === productId ? { ...product, quantityToReturn: quantity } : product
),
};
}
case 'SET_REASON': {
const { productId, reason } = action.payload;
return {
...state,
selectedProducts: state.selectedProducts.map((product) =>
product.productId === productId ? { ...product, reason } : product
),
};
}
case 'SET_RETURN_METHOD': {
const { productId, method } = action.payload;
return {
...state,
selectedProducts: state.selectedProducts.map((product) =>
product.productId === productId ? { ...product, returnMethod: method } : product
),
};
}
case 'NEXT_STEP': {
if (state.currentStep === 'PRODUCT_SELECTION') {
return { ...state, currentStep: 'REASON_SELECTION', selectedProducts: state.returnableProducts.filter(p => p.isSelected && p.quantityToReturn > 0) };
} else if (state.currentStep === 'REASON_SELECTION') {
return { ...state, currentStep: 'METHOD_SELECTION' };
} else if (state.currentStep === 'METHOD_SELECTION') {
return { ...state, currentStep: 'CONFIRMATION' };
}
return state;
}
case 'PREVIOUS_STEP': {
if (state.currentStep === 'REASON_SELECTION') {
return { ...state, currentStep: 'PRODUCT_SELECTION' };
} else if (state.currentStep === 'METHOD_SELECTION') {
return { ...state, currentStep: 'REASON_SELECTION' };
} else if (state.currentStep === 'CONFIRMATION') {
return { ...state, currentStep: 'METHOD_SELECTION' };
}
return state;
}
default:
return state;
}
};
Context Setup (TypeScript)
import React, { createContext, useContext, useReducer } from 'react';
interface ReturnContextProps {
state: ReturnState;
dispatch: React.Dispatch<ReturnAction>;
}
const ReturnContext = createContext<ReturnContextProps | undefined>(undefined);
export const ReturnProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(returnReducer, initialReturnState);
return (
<ReturnContext.Provider value={{ state, dispatch }}>
{children}
</ReturnContext.Provider>
);
};
export const useReturn = () => {
const context = useContext(ReturnContext);
if (!context) {
throw new Error('useReturn must be used within a ReturnProvider');
}
return context;
};
Implementation in Components
1. Product Selection Screen:
import React from 'react';
import { useReturn } from './ReturnContext';
const ProductSelectionScreen: React.FC = () => {
const { state, dispatch } = useReturn();
const handleToggleSelect = (productId: string) => {
dispatch({ type: 'TOGGLE_PRODUCT_SELECTION', payload: { productId } });
};
const handleQuantityChange = (productId: string, quantity: number) => {
dispatch({ type: 'SET_QUANTITY_TO_RETURN', payload: { productId, quantity } });
};
const handleNext = () => {
dispatch({ type: 'NEXT_STEP' });
};
const totalItemsToReturn = state.returnableProducts
.filter(p => p.isSelected)
.reduce((sum, p) => sum + p.quantityToReturn, 0);
const isNextDisabled = totalItemsToReturn === 0;
return (
<div>
<h2>Select Products to Return</h2>
<ul>
{state.returnableProducts.map((product) => (
<li key={product.productId}>
{product.product.name}, count: {product.returnSubItems.reduce((sum, item) => sum + item.quantity, 0)}
<input
type="checkbox"
checked={product.isSelected}
onChange={() => handleToggleSelect(product.productId)}
/>
{product.isSelected && (
<input
type="number"
value={product.quantityToReturn}
min={1}
max={product.returnSubItems.reduce((sum, item) => sum + item.quantity, 0)} // Assuming you want to limit by available quantity
onChange={(e) => handleQuantityChange(product.productId, parseInt(e.target.value))}
/>
)}
</li>
))}
</ul>
<div>Total Items Returning: {totalItemsToReturn}</div>
<button onClick={handleNext} disabled={isNextDisabled}>
Next
</button>
</div>
);
};
export default ProductSelectionScreen;
2. Reason Selection Screen:
import React from 'react';
import { useReturn } from './ReturnContext';
const ReasonSelectionScreen: React.FC = () => {
const { state, dispatch } = useReturn();
const handleReasonChange = (productId: string, reason: string) => {
dispatch({ type: 'SET_REASON', payload: { productId, reason } });
};
const handleNext = () => {
dispatch({ type: 'NEXT_STEP' });
};
const allReasonsSelected = state.selectedProducts.every(p => p.reason);
const isNextDisabled = !allReasonsSelected;
return (
<div>
<h2>Select Reasons for Return</h2>
{state.selectedProducts.map((product) => (
<div key={product.productId}>
<p>{product.product.name}</p>
<select
value={product.reason}
onChange={(e) => handleReasonChange(product.productId, e.target.value)}
>
<option value="">Select a reason</option>
{state.returnReasons.map((reason) => (
<option key={reason} value={reason}>
{reason}
</option>
))}
</select>
</div>
))}
<button onClick={handleNext} disabled={isNextDisabled}>
Next
</button>
</div>
);
};
export default ReasonSelectionScreen;
3. Method Selection Screen:
import React from 'react';
import { useReturn } from './ReturnContext';
const MethodSelectionScreen: React.FC = () => {
const { state, dispatch } = useReturn();
const availableMethods: ('POST' | 'DROP_OFF')[] = ['POST', 'DROP_OFF']; // Example, can be dynamic
const handleMethodChange = (productId: string, method: 'POST' | 'DROP_OFF') => {
dispatch({ type: 'SET_RETURN_METHOD', payload: { productId, method } });
};
const handleNext = () => {
// API call logic here using state.selectedProducts
console.log('Submitting Return Request:', state.selectedProducts);
dispatch({ type: 'NEXT_STEP' }); // Move to confirmation on success
};
const allMethodsSelected = state.selectedProducts.every(p => p.returnMethod);
const isNextDisabled = !allMethodsSelected;
return (
<div>
<h2>Select Return Methods</h2>
{state.selectedProducts.map((product) => (
<div key={product.productId}>
<p>{product.product.name}</p>
{availableMethods.map((method) => (
<label key={method}>
<input
type="radio"
name={`return-method-${product.productId}`}
value={method}
checked={product.returnMethod === method}
onChange={() => handleMethodChange(product.productId, method)}
/>
{method}
</label>
))}
</div>
))}
<button onClick={handleNext} disabled={isNextDisabled}>
Next
</button>
</div>
);
};
export default MethodSelectionScreen;
4. Confirmation Screen (Example):
import React from 'react';
import { useReturn } from './ReturnContext';
const ConfirmationScreen: React.FC = () => {
const { state } = useReturn();
const groupedByMethod = state.selectedProducts.reduce((acc, product) => {
const method = product.returnMethod || 'Unknown';
acc[method] = acc[method] || [];
acc[method].push(product);
return acc;
}, {} as Record<string, ReturnableProduct[]>);
return (
<div>
<h2>Return Confirmation</h2>
{Object.entries(groupedByMethod).map(([method, products]) => (
<div key={method}>
<h3>Return via {method}</h3>
<ul>
{products.map((product) => (
<li key={product.productId}>
{product.product.name} - Quantity: {product.quantityToReturn} - Reason: {product.reason}
</li>
))}
</ul>
</div>
))}
</div>
);
};
export default ConfirmationScreen;
Routing (using react-router-dom
)
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { ReturnProvider } from './ReturnContext';
import ProductSelectionScreen from './ProductSelectionScreen';
import ReasonSelectionScreen from './ReasonSelectionScreen';
import MethodSelectionScreen from './MethodSelectionScreen';
import ConfirmationScreen from './ConfirmationScreen';
const App: React.FC = () => {
return (
<ReturnProvider>
<Router>
<Routes>
<Route path="/return/products" element={<ProductSelectionScreen />} />
<Route path="/return/reasons" element={<ReasonSelectionScreen />} />
<Route path="/return/methods" element={<MethodSelectionScreen />} />
<Route path="/return/confirm" element={<ConfirmationScreen />} />
</Routes>
</Router>
</ReturnProvider>
);
};
export default App;
Special Scenarios:
-
Single product, 1 item:
- In your product selection screen, if there's only one product with a returnable quantity of 1, you can automatically select it and navigate to the reason selection screen.
-
Only one return method available:
- In your method selection screen, if only one return method is available for all selected products, you can automatically select that method for each product and skip the method selection screen, navigating directly to the confirmation. You'll need to adjust your reducer and component logic to handle this.
Persistence:
To persist the state across page refreshes, you can use localStorage
:
// In your ReturnProvider:
import { useEffect } from 'react';
export const ReturnProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(returnReducer, initialReturnState, () => {
const localState = localStorage.getItem('returnState');
return localState ? JSON.parse(localState) : initialReturnState;
});
useEffect(() => {
localStorage.setItem('returnState', JSON.stringify(state));
}, [state]);
// ... rest of the provider code
};
Important Considerations:
- API Integration: You'll need to dispatch an action (e.g.,
'SET_ORDER_OPTIONS'
) with the data from your API call to initialize the state. - Error Handling and Loading States: Implement loading indicators and error handling for API calls.
- Form Validation: Add more robust form validation, especially on the quantity and reason selection screens.
- Dynamic Return Methods: If the
returnMethods
in your API response are not always just 'POST' and 'DROP_OFF', you'll need to adjust theReturnableProduct
interface and the method selection logic accordingly. - Navigation: Use
useNavigate
fromreact-router-dom
to programmatically move between routes based on the user's actions.
This comprehensive data structure and state management approach should provide a solid foundation for implementing your order return feature. Remember to adapt the code to your specific API responses and UI requirements.