Add shipping dialog with tracking number to order page
Introduces a shipping dialog to the order details page, allowing users to optionally enter a tracking number when marking an order as shipped. Updates API client logic to better handle HTTP-only authentication cookies. Improves broadcast dialog validation and message handling.
This commit is contained in:
@@ -158,6 +158,8 @@ export default function OrderDetailsPage() {
|
||||
const [isAcknowledging, setIsAcknowledging] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [showShippingDialog, setShowShippingDialog] = useState(false);
|
||||
const [shippingTrackingNumber, setShippingTrackingNumber] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
@@ -262,7 +264,7 @@ export default function OrderDetailsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAsShipped = async () => {
|
||||
const handleMarkAsShipped = async (trackingNumber?: string) => {
|
||||
try {
|
||||
setIsMarkingShipped(true);
|
||||
|
||||
@@ -274,6 +276,12 @@ export default function OrderDetailsPage() {
|
||||
|
||||
if (response && response.message === "Order status updated successfully") {
|
||||
setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "shipped" } : null);
|
||||
|
||||
// If tracking number is provided, add it
|
||||
if (trackingNumber && trackingNumber.trim()) {
|
||||
await handleAddTrackingNumber(trackingNumber.trim());
|
||||
}
|
||||
|
||||
toast.success("Order marked as shipped successfully!");
|
||||
} else {
|
||||
throw new Error(response.error || "Failed to mark order as shipped");
|
||||
@@ -286,6 +294,48 @@ export default function OrderDetailsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTrackingNumber = async (trackingNumber: string) => {
|
||||
try {
|
||||
const authToken = document.cookie.split("Authorization=")[1];
|
||||
|
||||
const response = await fetchData(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}/tracking`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({ trackingNumber }),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) throw new Error(response.error);
|
||||
|
||||
// Update the local state
|
||||
setOrder(prevOrder => prevOrder ? {
|
||||
...prevOrder,
|
||||
trackingNumber: trackingNumber
|
||||
} : null);
|
||||
|
||||
toast.success("Tracking number added successfully!");
|
||||
} catch (err: any) {
|
||||
console.error("Failed to add tracking number:", err);
|
||||
toast.error(err.message || "Failed to add tracking number");
|
||||
}
|
||||
};
|
||||
|
||||
const handleShippingDialogConfirm = async () => {
|
||||
await handleMarkAsShipped(shippingTrackingNumber);
|
||||
setShowShippingDialog(false);
|
||||
setShippingTrackingNumber("");
|
||||
};
|
||||
|
||||
const handleShippingDialogCancel = () => {
|
||||
setShowShippingDialog(false);
|
||||
setShippingTrackingNumber("");
|
||||
};
|
||||
|
||||
const handleMarkAsAcknowledged = async () => {
|
||||
try {
|
||||
setIsAcknowledging(true);
|
||||
@@ -922,7 +972,7 @@ export default function OrderDetailsPage() {
|
||||
{order?.status === "acknowledged" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleMarkAsShipped}
|
||||
onClick={() => setShowShippingDialog(true)}
|
||||
disabled={isMarkingShipped}
|
||||
>
|
||||
{isMarkingShipped ? "Processing..." : "Mark as Shipped"}
|
||||
@@ -1082,6 +1132,41 @@ export default function OrderDetailsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shipping Dialog */}
|
||||
<AlertDialog open={showShippingDialog} onOpenChange={setShowShippingDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Mark Order as Shipped</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Mark this order as shipped. You can optionally add a tracking number.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="shipping-tracking">Tracking Number (Optional)</Label>
|
||||
<Input
|
||||
id="shipping-tracking"
|
||||
value={shippingTrackingNumber}
|
||||
onChange={(e) => setShippingTrackingNumber(e.target.value)}
|
||||
placeholder="Enter tracking number"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleShippingDialogCancel}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleShippingDialogConfirm}
|
||||
disabled={isMarkingShipped}
|
||||
>
|
||||
{isMarkingShipped ? "Processing..." : "Mark as Shipped"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
};
|
||||
|
||||
const sendBroadcast = async () => {
|
||||
if (!broadcastMessage.trim() && !selectedImage) {
|
||||
if ((!broadcastMessage || !broadcastMessage.trim()) && !selectedImage) {
|
||||
toast.warning("Please provide a message or image to broadcast.");
|
||||
return;
|
||||
}
|
||||
@@ -110,9 +110,8 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
||||
if (selectedImage) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedImage);
|
||||
if (broadcastMessage.trim()) {
|
||||
formData.append('message', broadcastMessage);
|
||||
}
|
||||
// Always append message, even if empty (backend will validate)
|
||||
formData.append('message', broadcastMessage || '');
|
||||
if (selectedProducts.length > 0) {
|
||||
formData.append('productIds', JSON.stringify(selectedProducts));
|
||||
}
|
||||
@@ -336,7 +335,7 @@ __italic text__
|
||||
</Button>
|
||||
<Button
|
||||
onClick={sendBroadcast}
|
||||
disabled={isSending || (broadcastMessage.length > 4096) || (!broadcastMessage.trim() && !selectedImage)}
|
||||
disabled={isSending || (broadcastMessage.length > 4096) || ((!broadcastMessage || !broadcastMessage.trim()) && !selectedImage)}
|
||||
className="bg-emerald-600 hover:bg-emerald-700"
|
||||
>
|
||||
{isSending ? "Sending..." : "Send Broadcast"}
|
||||
|
||||
@@ -600,6 +600,7 @@ export default function OrderTable() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,3 +49,4 @@ When adding new documentation:
|
||||
- **🔧 Configuration** - Environment variables, API setup
|
||||
- **🐛 Troubleshooting** - Common issues and fixes
|
||||
|
||||
|
||||
|
||||
@@ -151,14 +151,32 @@ function normalizeApiUrl(url: string): string {
|
||||
|
||||
/**
|
||||
* Get the authentication token from cookies or localStorage
|
||||
* Note: HTTP-only cookies cannot be read by JavaScript, so we return null
|
||||
* and rely on the browser to automatically include them in requests
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
if (typeof document === 'undefined') return null; // Guard for SSR
|
||||
|
||||
return document.cookie
|
||||
// Try localStorage first (for non-HTTP-only tokens)
|
||||
const localToken = localStorage.getItem('Authorization');
|
||||
if (localToken) {
|
||||
return localToken;
|
||||
}
|
||||
|
||||
// For HTTP-only cookies, we can't read them from JavaScript
|
||||
// The browser will automatically include them in requests
|
||||
// Check if the cookie exists (we can't read its value)
|
||||
const hasAuthCookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('Authorization='))
|
||||
?.split('=')[1] || localStorage.getItem('Authorization');
|
||||
.some(row => row.startsWith('Authorization='));
|
||||
|
||||
if (hasAuthCookie) {
|
||||
// Return a special marker to indicate the cookie exists
|
||||
// The actual token will be sent automatically by the browser
|
||||
return 'HTTP_ONLY_COOKIE';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,9 +206,11 @@ function createApiHeaders(token?: string | null, customHeaders: Record<string, s
|
||||
});
|
||||
|
||||
const authToken = token || getAuthToken();
|
||||
if (authToken) {
|
||||
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||
// Only add Authorization header for non-HTTP-only tokens
|
||||
headers.set('authorization', `Bearer ${authToken}`);
|
||||
}
|
||||
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||
|
||||
return headers;
|
||||
}
|
||||
@@ -273,10 +293,11 @@ export async function fetchClient<T>(
|
||||
...(headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (authToken) {
|
||||
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||
// Backend expects "Bearer TOKEN" format
|
||||
requestHeaders['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "a10b9e0",
|
||||
"buildTime": "2025-08-31T17:56:33.446Z"
|
||||
"commitHash": "8554481",
|
||||
"buildTime": "2025-09-17T17:02:11.044Z"
|
||||
}
|
||||
Reference in New Issue
Block a user