feat(orders): implement return request functionality and order timeline

- Added RequestReturnDialog component for initiating return requests.
- Enhanced OrderDetailPageView to handle return requests and display order timeline.
- Updated OrderActions to include return request button based on order status.
- Introduced OrderTimeline component to visualize order events.
- Modified order-related types and constants to support return functionality.
- Updated UI components for better styling and user experience.

This commit improves the order management system by allowing users to request returns and view the timeline of their orders.
This commit is contained in:
2026-03-07 19:47:55 +03:00
parent 0f91d3dc05
commit 5f7c3cece9
20 changed files with 336 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" /> import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,6 +1,20 @@
const path = require("path");
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
transpilePackages: ["@repo/convex", "@repo/types", "@repo/utils"], transpilePackages: ["@repo/convex", "@repo/types", "@repo/utils"],
turbopack: {
root: path.join(__dirname, "..", ".."),
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "res.cloudinary.com",
pathname: "/**",
},
],
},
// PPR: enable when using Next.js canary. Uncomment and add experimental_ppr to PDP page: // PPR: enable when using Next.js canary. Uncomment and add experimental_ppr to PDP page:
// experimental: { ppr: "incremental" }, // experimental: { ppr: "incremental" },
}; };

View File

@@ -0,0 +1,12 @@
import { Suspense } from "react";
import { ShopProductGridSkeleton } from "@/components/shop/state/ShopProductGridSkeleton";
export default function ShopLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<Suspense fallback={<ShopProductGridSkeleton />}>{children}</Suspense>
);
}

View File

@@ -6,7 +6,6 @@ import {
useProductSearch, useProductSearch,
useClickOutside, useClickOutside,
SEARCH_CATEGORIES, SEARCH_CATEGORIES,
MIN_SEARCH_LENGTH,
} from "@/lib/search"; } from "@/lib/search";
import type { SearchCategory } from "@/lib/search"; import type { SearchCategory } from "@/lib/search";
import { SearchResultsPanel } from "@/components/search/SearchResultsPanel"; import { SearchResultsPanel } from "@/components/search/SearchResultsPanel";
@@ -39,15 +38,6 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) {
setDropdownOpen(false); setDropdownOpen(false);
} }
function handleSearchButtonClick() {
if (search.query.length >= MIN_SEARCH_LENGTH) {
router.push(`/shop?search=${encodeURIComponent(search.query)}`);
search.close();
} else {
search.open();
}
}
const isDesktop = variant === "desktop"; const isDesktop = variant === "desktop";
return ( return (
@@ -153,8 +143,8 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) {
} }
className={ className={
isDesktop isDesktop
? "flex-1 bg-transparent py-3 pl-4 pr-3 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8]" ? "flex-1 bg-transparent py-3 pl-4 pr-5 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8]"
: "flex-1 border-none bg-transparent pl-3 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8] focus:ring-0" : "flex-1 border-none bg-transparent pl-3 pr-4 text-sm text-[#1a2e2d] outline-none placeholder:text-[#8aa9a8] focus:ring-0"
} }
role="combobox" role="combobox"
aria-expanded={search.isOpen && search.showResults} aria-expanded={search.isOpen && search.showResults}
@@ -168,27 +158,6 @@ export function HeaderSearchBar({ variant }: HeaderSearchBarProps) {
autoComplete="off" autoComplete="off"
/> />
{/* Search Button */}
<button
onClick={handleSearchButtonClick}
aria-label="Search"
className={`${isDesktop ? "mr-1.5 " : ""}flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#236f6b] text-white transition-colors hover:bg-[#38a99f]`}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</button>
{/* Results Panel */} {/* Results Panel */}
{(search.showResults || search.showMinCharsHint) && ( {(search.showResults || search.showMinCharsHint) && (
<SearchResultsPanel <SearchResultsPanel

View File

@@ -1,8 +1,11 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "convex/react";
import { Breadcrumbs, toast } from "@heroui/react"; import { Breadcrumbs, toast } from "@heroui/react";
import Link from "next/link"; import Link from "next/link";
import { api } from "../../../../../convex/_generated/api";
import type { Id } from "../../../../../convex/_generated/dataModel";
import { ORDERS_PATH, useOrderDetail, useOrderActions } from "@/lib/orders"; import { ORDERS_PATH, useOrderDetail, useOrderActions } from "@/lib/orders";
import { useCartSession } from "@/lib/session"; import { useCartSession } from "@/lib/session";
import { OrderHeader } from "./detail/OrderHeader"; import { OrderHeader } from "./detail/OrderHeader";
@@ -11,8 +14,10 @@ import { OrderPriceSummary } from "./detail/OrderPriceSummary";
import { OrderAddresses } from "./detail/OrderAddresses"; import { OrderAddresses } from "./detail/OrderAddresses";
import { OrderTrackingInfo } from "./detail/OrderTrackingInfo"; import { OrderTrackingInfo } from "./detail/OrderTrackingInfo";
import { OrderActions } from "./detail/OrderActions"; import { OrderActions } from "./detail/OrderActions";
import { OrderTimeline } from "./detail/OrderTimeline";
import { CancelOrderDialog } from "./actions/CancelOrderDialog"; import { CancelOrderDialog } from "./actions/CancelOrderDialog";
import { ReorderConfirmDialog } from "./actions/ReorderConfirmDialog"; import { ReorderConfirmDialog } from "./actions/ReorderConfirmDialog";
import { RequestReturnDialog } from "./actions/RequestReturnDialog";
import { OrderDetailSkeleton } from "./state/OrderDetailSkeleton"; import { OrderDetailSkeleton } from "./state/OrderDetailSkeleton";
interface Props { interface Props {
@@ -21,11 +26,16 @@ interface Props {
export function OrderDetailPageView({ orderId }: Props) { export function OrderDetailPageView({ orderId }: Props) {
const { order, isLoading } = useOrderDetail(orderId); const { order, isLoading } = useOrderDetail(orderId);
const { cancelOrder, isCancelling, reorderItems, isReordering } = const timelineEvents = useQuery(
api.orders.getTimeline,
order ? { orderId: orderId as Id<"orders"> } : "skip",
) ?? [];
const { cancelOrder, isCancelling, requestReturn, isRequestingReturn, reorderItems, isReordering } =
useOrderActions(); useOrderActions();
const { sessionId } = useCartSession(); const { sessionId } = useCartSession();
const [cancelDialogOpen, setCancelDialogOpen] = useState(false); const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [reorderDialogOpen, setReorderDialogOpen] = useState(false); const [reorderDialogOpen, setReorderDialogOpen] = useState(false);
if (isLoading) return <OrderDetailSkeleton />; if (isLoading) return <OrderDetailSkeleton />;
@@ -54,6 +64,16 @@ export function OrderDetailPageView({ orderId }: Props) {
} }
}; };
const handleConfirmReturn = async () => {
const result = await requestReturn(orderId);
setReturnDialogOpen(false);
if (result.success) {
toast.success("Return requested. We'll be in touch with next steps.");
} else {
toast.danger("Failed to submit return request. Please try again.");
}
};
const handleConfirmReorder = async () => { const handleConfirmReorder = async () => {
const { added, skipped } = await reorderItems(order.items, sessionId); const { added, skipped } = await reorderItems(order.items, sessionId);
setReorderDialogOpen(false); setReorderDialogOpen(false);
@@ -128,11 +148,23 @@ export function OrderDetailPageView({ orderId }: Props) {
order={order} order={order}
onCancel={() => setCancelDialogOpen(true)} onCancel={() => setCancelDialogOpen(true)}
isCancelling={isCancelling} isCancelling={isCancelling}
onRequestReturn={() => setReturnDialogOpen(true)}
isRequestingReturn={isRequestingReturn}
onReorder={() => setReorderDialogOpen(true)} onReorder={() => setReorderDialogOpen(true)}
isReordering={isReordering} isReordering={isReordering}
/> />
{/* Timeline */}
<OrderTimeline events={timelineEvents} />
{/* Dialogs */} {/* Dialogs */}
<RequestReturnDialog
isOpen={returnDialogOpen}
onClose={() => setReturnDialogOpen(false)}
onConfirm={handleConfirmReturn}
isRequesting={isRequestingReturn}
orderNumber={order.orderNumber}
/>
<CancelOrderDialog <CancelOrderDialog
isOpen={cancelDialogOpen} isOpen={cancelDialogOpen}
onClose={() => setCancelDialogOpen(false)} onClose={() => setCancelDialogOpen(false)}

View File

@@ -0,0 +1,60 @@
"use client";
import { AlertDialog, Button, Spinner } from "@heroui/react";
interface Props {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
isRequesting: boolean;
orderNumber: string;
}
export function RequestReturnDialog({
isOpen,
onClose,
onConfirm,
isRequesting,
orderNumber,
}: Props) {
return (
<AlertDialog>
<AlertDialog.Backdrop isOpen={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
<AlertDialog.Container>
<AlertDialog.Dialog>
<AlertDialog.Header>
<AlertDialog.Icon status="warning" />
<AlertDialog.Heading>Request a Return?</AlertDialog.Heading>
</AlertDialog.Header>
<AlertDialog.Body>
<p className="text-sm text-gray-600">
You are requesting a return for order{" "}
<span className="font-medium text-[#1a2e2d]">{orderNumber}</span>.
Our team will review your request and contact you with next steps.
</p>
</AlertDialog.Body>
<AlertDialog.Footer>
<Button variant="outline" slot="close" isDisabled={isRequesting}>
Cancel
</Button>
<Button
variant="primary"
onPress={onConfirm}
isDisabled={isRequesting}
>
{isRequesting ? (
<>
<Spinner size="sm" />
Submitting
</>
) : (
"Yes, Request Return"
)}
</Button>
</AlertDialog.Footer>
</AlertDialog.Dialog>
</AlertDialog.Container>
</AlertDialog.Backdrop>
</AlertDialog>
);
}

View File

@@ -2,13 +2,15 @@
import { Button, Spinner } from "@heroui/react"; import { Button, Spinner } from "@heroui/react";
import Link from "next/link"; import Link from "next/link";
import { CANCELLABLE_STATUSES, ORDERS_PATH } from "@/lib/orders"; import { CANCELLABLE_STATUSES, RETURNABLE_STATUSES, ORDERS_PATH } from "@/lib/orders";
import type { OrderDetail } from "@/lib/orders"; import type { OrderDetail } from "@/lib/orders";
interface Props { interface Props {
order: OrderDetail; order: OrderDetail;
onCancel: () => void; onCancel: () => void;
isCancelling: boolean; isCancelling: boolean;
onRequestReturn: () => void;
isRequestingReturn: boolean;
onReorder: () => void; onReorder: () => void;
isReordering: boolean; isReordering: boolean;
} }
@@ -19,12 +21,18 @@ export function OrderActions({
order, order,
onCancel, onCancel,
isCancelling, isCancelling,
onRequestReturn,
isRequestingReturn,
onReorder, onReorder,
isReordering, isReordering,
}: Props) { }: Props) {
const canCancel = (CANCELLABLE_STATUSES as readonly string[]).includes( const canCancel = (CANCELLABLE_STATUSES as readonly string[]).includes(
order.status, order.status,
); );
const canRequestReturn =
(RETURNABLE_STATUSES as readonly string[]).includes(order.status) &&
!order.returnRequestedAt &&
order.paymentStatus !== "refunded";
const canReorder = (REORDERABLE_STATUSES as readonly string[]).includes( const canReorder = (REORDERABLE_STATUSES as readonly string[]).includes(
order.status, order.status,
); );
@@ -49,6 +57,30 @@ export function OrderActions({
</Button> </Button>
)} )}
{canRequestReturn && (
<Button
variant="outline"
className="w-full md:w-auto"
onPress={onRequestReturn}
isDisabled={isRequestingReturn}
>
{isRequestingReturn ? (
<>
<Spinner size="sm" />
Submitting
</>
) : (
"Request Return"
)}
</Button>
)}
{order.returnRequestedAt && order.status === "delivered" && (
<span className="inline-flex w-full items-center justify-center rounded-md border border-gray-200 px-4 py-2 text-sm font-medium text-gray-500 md:w-auto">
Return Requested
</span>
)}
{canReorder && ( {canReorder && (
<Button <Button
variant="outline" variant="outline"

View File

@@ -35,7 +35,7 @@ function AddressLines({
export function OrderAddresses({ shippingAddress, billingAddress }: Props) { export function OrderAddresses({ shippingAddress, billingAddress }: Props) {
return ( return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card> <Card className="rounded-xl p-5">
<Card.Header> <Card.Header>
<Card.Title className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <Card.Title className="text-sm font-semibold uppercase tracking-wide text-gray-500">
Shipping Address Shipping Address
@@ -46,7 +46,7 @@ export function OrderAddresses({ shippingAddress, billingAddress }: Props) {
</Card.Content> </Card.Content>
</Card> </Card>
<Card> <Card className="rounded-xl p-5">
<Card.Header> <Card.Header>
<Card.Title className="text-sm font-semibold uppercase tracking-wide text-gray-500"> <Card.Title className="text-sm font-semibold uppercase tracking-wide text-gray-500">
Billing Address Billing Address

View File

@@ -63,7 +63,7 @@ export function OrderLineItems({ items, currency }: Props) {
const scrollable = items.length > 4; const scrollable = items.length > 4;
return ( return (
<Card> <Card className="rounded-xl p-5">
<Card.Header> <Card.Header>
<Card.Title className="text-base"> <Card.Title className="text-base">
Items ({items.length}) Items ({items.length})

View File

@@ -19,7 +19,7 @@ export function OrderPriceSummary({
currency, currency,
}: Props) { }: Props) {
return ( return (
<Card> <Card className="rounded-xl p-5">
<Card.Header> <Card.Header>
<Card.Title className="text-base">Order Summary</Card.Title> <Card.Title className="text-base">Order Summary</Card.Title>
</Card.Header> </Card.Header>

View File

@@ -0,0 +1,134 @@
"use client";
interface TimelineEvent {
_id: string;
eventType: string;
source: string;
fromStatus?: string;
toStatus?: string;
createdAt: number;
}
interface Props {
events: TimelineEvent[];
}
const STATUS_LABELS: Record<string, string> = {
pending: "Pending",
confirmed: "Confirmed",
processing: "Processing",
shipped: "Shipped",
delivered: "Delivered",
cancelled: "Cancelled",
refunded: "Refunded",
};
function getEventLabel(event: TimelineEvent): string {
switch (event.eventType) {
case "status_change":
if (event.toStatus) {
return `Order ${STATUS_LABELS[event.toStatus] ?? event.toStatus}`;
}
return "Status updated";
case "customer_cancel":
return "Order cancelled by you";
case "return_requested":
return "Return requested";
case "return_received":
return "Return received";
case "refund":
return "Refund issued";
case "tracking_update":
return "Tracking updated";
case "label_created":
return "Shipping label created";
default:
return "Order updated";
}
}
function getEventDescription(event: TimelineEvent): string | null {
if (
event.eventType === "status_change" &&
event.fromStatus &&
event.toStatus
) {
return `${STATUS_LABELS[event.fromStatus] ?? event.fromStatus}${STATUS_LABELS[event.toStatus] ?? event.toStatus}`;
}
if (event.eventType === "return_requested") {
return "Our team will review and contact you with next steps.";
}
if (event.eventType === "refund") {
return "Your refund has been processed. Allow 510 business days.";
}
return null;
}
function getDotColor(eventType: string): string {
switch (eventType) {
case "customer_cancel":
return "bg-[#f2705a]";
case "return_requested":
case "return_received":
return "bg-[#f4a13a]";
case "refund":
return "bg-green-500";
default:
return "bg-[#38a99f]";
}
}
function formatTimestamp(ts: number): string {
return new Intl.DateTimeFormat("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(ts));
}
export function OrderTimeline({ events }: Props) {
if (events.length === 0) return null;
return (
<section className="rounded-xl border border-gray-200 bg-white p-6">
<h2 className="mb-4 text-sm font-semibold text-[#1a2e2d]">
Order Timeline
</h2>
<ol className="relative space-y-0">
{events.map((event, index) => {
const isLast = index === events.length - 1;
return (
<li key={event._id} className="relative flex gap-4">
{/* Vertical line + dot */}
<div className="flex flex-col items-center">
<div
className={`mt-1 size-2.5 shrink-0 rounded-full ${getDotColor(event.eventType)}`}
/>
{!isLast && (
<div className="mt-1 w-px flex-1 bg-gray-200" />
)}
</div>
{/* Content */}
<div className={`pb-6 ${isLast ? "pb-0" : ""}`}>
<p className="text-sm font-medium text-[#1a2e2d]">
{getEventLabel(event)}
</p>
{getEventDescription(event) && (
<p className="mt-0.5 text-xs text-gray-500">
{getEventDescription(event)}
</p>
)}
<time className="mt-1 block text-xs text-gray-400">
{formatTimestamp(event.createdAt)}
</time>
</div>
</li>
);
})}
</ol>
</section>
);
}

View File

@@ -34,7 +34,7 @@ export function OrderTrackingInfo({
const isPending = status === "pending" || status === "confirmed"; const isPending = status === "pending" || status === "confirmed";
return ( return (
<Card> <Card className="rounded-xl p-5">
<Card.Header> <Card.Header>
<Card.Title className="text-base">Shipping & Tracking</Card.Title> <Card.Title className="text-base">Shipping & Tracking</Card.Title>
</Card.Header> </Card.Header>

View File

@@ -24,7 +24,7 @@ export function OrderCard({ order, onViewDetails }: Props) {
return ( return (
<article> <article>
<Card className="w-full"> <Card className="w-full rounded-xl p-5">
<Card.Header className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <Card.Header className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<h3 className="font-[family-name:var(--font-fraunces)] text-base font-semibold text-[#1a2e2d] md:text-lg"> <h3 className="font-[family-name:var(--font-fraunces)] text-base font-semibold text-[#1a2e2d] md:text-lg">
{order.orderNumber} {order.orderNumber}

View File

@@ -91,7 +91,7 @@ export function CategorySection() {
<Link <Link
key={href} key={href}
href={href} href={href}
className="flex shrink-0 flex-col items-center gap-3 rounded-2xl transition-[transform] duration-[var(--transition-base)] hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2 focus:ring-offset-[var(--brand-mist)] lg:shrink" className="flex shrink-0 flex-col items-center gap-3 rounded-2xl transition-[transform] duration-[var(--transition-base)] hover:scale-[1.02] lg:shrink"
style={{ scrollSnapAlign: "start" }} style={{ scrollSnapAlign: "start" }}
> >
<span <span

View File

@@ -3,6 +3,7 @@ import type { OrderStatus, PaymentStatus } from "./types";
export const ORDERS_PATH = "/account/orders"; export const ORDERS_PATH = "/account/orders";
export const ORDERS_PAGE_SIZE = 10; export const ORDERS_PAGE_SIZE = 10;
export const CANCELLABLE_STATUSES = ["confirmed"] as const; export const CANCELLABLE_STATUSES = ["confirmed"] as const;
export const RETURNABLE_STATUSES = ["delivered"] as const;
export const ORDER_STATUS_CONFIG: Record< export const ORDER_STATUS_CONFIG: Record<
OrderStatus, OrderStatus,
@@ -43,6 +44,16 @@ export const ORDER_STATUS_CONFIG: Record<
colorClass: "bg-[#fce0da] text-[#f2705a]", colorClass: "bg-[#fce0da] text-[#f2705a]",
chipVariant: "default", chipVariant: "default",
}, },
return: {
label: "Return Requested",
colorClass: "bg-orange-50 text-orange-700",
chipVariant: "default",
},
completed: {
label: "Completed",
colorClass: "bg-teal-50 text-teal-700",
chipVariant: "default",
},
}; };
export const PAYMENT_STATUS_CONFIG: Record< export const PAYMENT_STATUS_CONFIG: Record<

View File

@@ -11,6 +11,7 @@ export {
ORDERS_PATH, ORDERS_PATH,
ORDERS_PAGE_SIZE, ORDERS_PAGE_SIZE,
CANCELLABLE_STATUSES, CANCELLABLE_STATUSES,
RETURNABLE_STATUSES,
ORDER_STATUS_CONFIG, ORDER_STATUS_CONFIG,
PAYMENT_STATUS_CONFIG, PAYMENT_STATUS_CONFIG,
ORDER_TAB_FILTERS, ORDER_TAB_FILTERS,

View File

@@ -5,7 +5,9 @@ export type OrderStatus =
| "shipped" | "shipped"
| "delivered" | "delivered"
| "cancelled" | "cancelled"
| "refunded"; | "refunded"
| "return"
| "completed";
export type PaymentStatus = "pending" | "paid" | "failed" | "refunded"; export type PaymentStatus = "pending" | "paid" | "failed" | "refunded";
@@ -71,6 +73,8 @@ export interface OrderDetail extends OrderSummary {
shippedAt?: number; shippedAt?: number;
actualDelivery?: number; actualDelivery?: number;
notes?: string; notes?: string;
returnRequestedAt?: number;
returnReceivedAt?: number;
} }
export interface OrderCancellationResult { export interface OrderCancellationResult {

View File

@@ -10,6 +10,8 @@ import type { OrderLineItem } from "./types";
export function useOrderActions(): { export function useOrderActions(): {
cancelOrder: (orderId: string) => Promise<{ success: boolean }>; cancelOrder: (orderId: string) => Promise<{ success: boolean }>;
isCancelling: boolean; isCancelling: boolean;
requestReturn: (orderId: string) => Promise<{ success: boolean }>;
isRequestingReturn: boolean;
reorderItems: ( reorderItems: (
items: OrderLineItem[], items: OrderLineItem[],
sessionId?: string, sessionId?: string,
@@ -17,9 +19,11 @@ export function useOrderActions(): {
isReordering: boolean; isReordering: boolean;
} { } {
const [isCancelling, setIsCancelling] = useState(false); const [isCancelling, setIsCancelling] = useState(false);
const [isRequestingReturn, setIsRequestingReturn] = useState(false);
const [isReordering, setIsReordering] = useState(false); const [isReordering, setIsReordering] = useState(false);
const cancelMutation = useMutation(api.orders.cancel); const cancelMutation = useMutation(api.orders.cancel);
const requestReturnMutation = useMutation(api.orders.requestReturn);
const addItemMutation = useMutation(api.carts.addItem); const addItemMutation = useMutation(api.carts.addItem);
const router = useRouter(); const router = useRouter();
@@ -35,6 +39,18 @@ export function useOrderActions(): {
} }
} }
async function requestReturn(orderId: string): Promise<{ success: boolean }> {
setIsRequestingReturn(true);
try {
await requestReturnMutation({ id: orderId as Id<"orders"> });
return { success: true };
} catch {
return { success: false };
} finally {
setIsRequestingReturn(false);
}
}
async function reorderItems( async function reorderItems(
items: OrderLineItem[], items: OrderLineItem[],
sessionId?: string, sessionId?: string,
@@ -63,5 +79,5 @@ export function useOrderActions(): {
return { added, skipped }; return { added, skipped };
} }
return { cancelOrder, isCancelling, reorderItems, isReordering }; return { cancelOrder, isCancelling, requestReturn, isRequestingReturn, reorderItems, isReordering };
} }

View File

@@ -15,7 +15,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
@@ -24,14 +24,17 @@
"target": "ES2017", "target": "ES2017",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
".next/types/**/*.ts", ".next/types/**/*.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx",
".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"