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

View File

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

View File

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

View File

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

View File

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

View File

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