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 [isAcknowledging, setIsAcknowledging] = useState(false);
|
||||||
const [isCancelling, setIsCancelling] = useState(false);
|
const [isCancelling, setIsCancelling] = useState(false);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
const [showShippingDialog, setShowShippingDialog] = useState(false);
|
||||||
|
const [shippingTrackingNumber, setShippingTrackingNumber] = useState("");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -262,7 +264,7 @@ export default function OrderDetailsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAsShipped = async () => {
|
const handleMarkAsShipped = async (trackingNumber?: string) => {
|
||||||
try {
|
try {
|
||||||
setIsMarkingShipped(true);
|
setIsMarkingShipped(true);
|
||||||
|
|
||||||
@@ -274,6 +276,12 @@ export default function OrderDetailsPage() {
|
|||||||
|
|
||||||
if (response && response.message === "Order status updated successfully") {
|
if (response && response.message === "Order status updated successfully") {
|
||||||
setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "shipped" } : null);
|
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!");
|
toast.success("Order marked as shipped successfully!");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || "Failed to mark order as shipped");
|
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 () => {
|
const handleMarkAsAcknowledged = async () => {
|
||||||
try {
|
try {
|
||||||
setIsAcknowledging(true);
|
setIsAcknowledging(true);
|
||||||
@@ -922,7 +972,7 @@ export default function OrderDetailsPage() {
|
|||||||
{order?.status === "acknowledged" && (
|
{order?.status === "acknowledged" && (
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleMarkAsShipped}
|
onClick={() => setShowShippingDialog(true)}
|
||||||
disabled={isMarkingShipped}
|
disabled={isMarkingShipped}
|
||||||
>
|
>
|
||||||
{isMarkingShipped ? "Processing..." : "Mark as Shipped"}
|
{isMarkingShipped ? "Processing..." : "Mark as Shipped"}
|
||||||
@@ -1082,6 +1132,41 @@ export default function OrderDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendBroadcast = async () => {
|
const sendBroadcast = async () => {
|
||||||
if (!broadcastMessage.trim() && !selectedImage) {
|
if ((!broadcastMessage || !broadcastMessage.trim()) && !selectedImage) {
|
||||||
toast.warning("Please provide a message or image to broadcast.");
|
toast.warning("Please provide a message or image to broadcast.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,9 +110,8 @@ export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps)
|
|||||||
if (selectedImage) {
|
if (selectedImage) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', selectedImage);
|
formData.append('file', selectedImage);
|
||||||
if (broadcastMessage.trim()) {
|
// Always append message, even if empty (backend will validate)
|
||||||
formData.append('message', broadcastMessage);
|
formData.append('message', broadcastMessage || '');
|
||||||
}
|
|
||||||
if (selectedProducts.length > 0) {
|
if (selectedProducts.length > 0) {
|
||||||
formData.append('productIds', JSON.stringify(selectedProducts));
|
formData.append('productIds', JSON.stringify(selectedProducts));
|
||||||
}
|
}
|
||||||
@@ -336,7 +335,7 @@ __italic text__
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={sendBroadcast}
|
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"
|
className="bg-emerald-600 hover:bg-emerald-700"
|
||||||
>
|
>
|
||||||
{isSending ? "Sending..." : "Send Broadcast"}
|
{isSending ? "Sending..." : "Send Broadcast"}
|
||||||
|
|||||||
@@ -600,6 +600,7 @@ export default function OrderTable() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,3 +49,4 @@ When adding new documentation:
|
|||||||
- **🔧 Configuration** - Environment variables, API setup
|
- **🔧 Configuration** - Environment variables, API setup
|
||||||
- **🐛 Troubleshooting** - Common issues and fixes
|
- **🐛 Troubleshooting** - Common issues and fixes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -151,14 +151,32 @@ function normalizeApiUrl(url: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the authentication token from cookies or localStorage
|
* 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 {
|
export function getAuthToken(): string | null {
|
||||||
if (typeof document === 'undefined') return null; // Guard for SSR
|
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('; ')
|
.split('; ')
|
||||||
.find(row => row.startsWith('Authorization='))
|
.some(row => row.startsWith('Authorization='));
|
||||||
?.split('=')[1] || localStorage.getItem('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();
|
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}`);
|
headers.set('authorization', `Bearer ${authToken}`);
|
||||||
}
|
}
|
||||||
|
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
@@ -273,10 +293,11 @@ export async function fetchClient<T>(
|
|||||||
...(headers as Record<string, string>),
|
...(headers as Record<string, string>),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (authToken) {
|
if (authToken && authToken !== 'HTTP_ONLY_COOKIE') {
|
||||||
// Backend expects "Bearer TOKEN" format
|
// Backend expects "Bearer TOKEN" format
|
||||||
requestHeaders['Authorization'] = `Bearer ${authToken}`;
|
requestHeaders['Authorization'] = `Bearer ${authToken}`;
|
||||||
}
|
}
|
||||||
|
// For HTTP_ONLY_COOKIE, the browser will automatically include the cookie
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "a10b9e0",
|
"commitHash": "8554481",
|
||||||
"buildTime": "2025-08-31T17:56:33.446Z"
|
"buildTime": "2025-09-17T17:02:11.044Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user