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:
NotII
2025-09-22 00:45:29 +01:00
parent 8554481282
commit 74b7aa4877
6 changed files with 121 additions and 14 deletions

View File

@@ -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>
);

View File

@@ -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"}

View File

@@ -600,6 +600,7 @@ export default function OrderTable() {
</Button>
</div>
</div>
</div>
</div>
);

View File

@@ -49,3 +49,4 @@ When adding new documentation:
- **🔧 Configuration** - Environment variables, API setup
- **🐛 Troubleshooting** - Common issues and fixes

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
{
"commitHash": "a10b9e0",
"buildTime": "2025-08-31T17:56:33.446Z"
"commitHash": "8554481",
"buildTime": "2025-09-17T17:02:11.044Z"
}