fix: single order checkout added
This commit is contained in:
parent
c2b4c4a690
commit
b548c59484
4 changed files with 19 additions and 180 deletions
|
|
@ -1,19 +0,0 @@
|
|||
import { redirect } from "next/navigation";
|
||||
import { getCart } from "@/lib/api/cart";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { getAddresses } from "@/lib/api/addresses";
|
||||
import { CheckoutForm } from "@/components/customer/checkout-form";
|
||||
|
||||
export default async function CheckoutPage() {
|
||||
await requireSession("/checkout");
|
||||
const [cart, addresses] = await Promise.all([getCart(), getAddresses()]);
|
||||
|
||||
if (cart.groups.length === 0) redirect("/cart");
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-2xl flex-1 flex-col gap-6 px-4 py-6">
|
||||
<h1 className="text-lg font-bold">تکمیل سفارش</h1>
|
||||
<CheckoutForm cart={cart} addresses={addresses} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { submitCheckout } from "@/lib/api/checkout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api";
|
||||
|
||||
const PAYMENT_LABELS_FA: Record<PaymentMethod, string> = {
|
||||
wallet: "کیف پول وینوفای",
|
||||
online: "درگاه بانکی",
|
||||
cash_on_delivery: "پرداخت در محل",
|
||||
};
|
||||
|
||||
export function CheckoutForm({ cart, addresses }: { cart: Cart; addresses: Address[] }) {
|
||||
const router = useRouter();
|
||||
const [addressUuid, setAddressUuid] = useState(addresses.find((a) => a.is_default)?.uuid ?? addresses[0]?.uuid ?? "");
|
||||
const [deliveryType, setDeliveryType] = useState<DeliveryType>("express");
|
||||
const [scheduledAt, setScheduledAt] = useState("");
|
||||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("cash_on_delivery");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!addressUuid) {
|
||||
setError("لطفاً یک آدرس انتخاب کنید.");
|
||||
return;
|
||||
}
|
||||
if (deliveryType === "scheduled" && !scheduledAt) {
|
||||
setError("لطفاً زمان ارسال را مشخص کنید.");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await submitCheckout({
|
||||
address_uuid: addressUuid,
|
||||
delivery_type: deliveryType,
|
||||
scheduled_at: deliveryType === "scheduled" ? new Date(scheduledAt).toISOString() : null,
|
||||
payment_method: paymentMethod,
|
||||
notes,
|
||||
});
|
||||
if (result.ok) {
|
||||
router.push(`/order-groups/${result.data.uuid}`);
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 rounded-2xl border border-border p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">برای ثبت سفارش ابتدا یک آدرس اضافه کنید.</p>
|
||||
<Link href="/addresses">
|
||||
<Button>افزودن آدرس</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">آدرس تحویل</h2>
|
||||
{addresses.map((address) => (
|
||||
<label
|
||||
key={address.uuid}
|
||||
className={`flex cursor-pointer items-start gap-3 rounded-2xl border p-3 text-sm ${addressUuid === address.uuid ? "border-primary" : "border-border"}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="address"
|
||||
checked={addressUuid === address.uuid}
|
||||
onChange={() => setAddressUuid(address.uuid)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-medium">{address.title || address.full_address}</span>
|
||||
<span className="block text-muted-foreground">{address.full_address}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
<Link href="/addresses" className="text-xs text-primary underline underline-offset-4">
|
||||
مدیریت آدرسها
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">زمان ارسال</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeliveryType("express")}
|
||||
className={`flex-1 rounded-xl border px-3 py-2 text-sm ${deliveryType === "express" ? "border-primary bg-primary/10" : "border-border"}`}
|
||||
>
|
||||
ارسال فوری
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeliveryType("scheduled")}
|
||||
className={`flex-1 rounded-xl border px-3 py-2 text-sm ${deliveryType === "scheduled" ? "border-primary bg-primary/10" : "border-border"}`}
|
||||
>
|
||||
زمانبندی شده
|
||||
</button>
|
||||
</div>
|
||||
{deliveryType === "scheduled" && (
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduledAt}
|
||||
onChange={(e) => setScheduledAt(e.target.value)}
|
||||
className="h-11 rounded-xl border border-border bg-background px-3 text-sm"
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">روش پرداخت</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{(Object.keys(PAYMENT_LABELS_FA) as PaymentMethod[]).map((method) => (
|
||||
<label
|
||||
key={method}
|
||||
className={`flex cursor-pointer items-center gap-3 rounded-2xl border p-3 text-sm ${paymentMethod === method ? "border-primary" : "border-border"}`}
|
||||
>
|
||||
<input type="radio" name="payment" checked={paymentMethod === method} onChange={() => setPaymentMethod(method)} />
|
||||
{PAYMENT_LABELS_FA[method]}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">توضیحات (اختیاری)</h2>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={2}
|
||||
className="rounded-xl border border-border bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{error && <p className="rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex items-center justify-between rounded-2xl border border-border p-4 font-semibold">
|
||||
<span>مبلغ قابل پرداخت</span>
|
||||
<span>{cart.grand_total.toLocaleString("fa-IR")} تومان</span>
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="lg" disabled={isPending}>
|
||||
{isPending ? "در حال ثبت سفارش..." : "ثبت نهایی سفارش"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -37,14 +37,16 @@ export function CheckoutPayment({
|
|||
return;
|
||||
}
|
||||
startTransition(async () => {
|
||||
// submitCheckout processes the whole cart at once (splits into per-store orders
|
||||
// server-side), not just this store — see PageProps note on the confirm page.
|
||||
const result = await submitCheckout({
|
||||
address_uuid: addressUuid,
|
||||
delivery_type: "express",
|
||||
scheduled_at: null,
|
||||
payment_method: method,
|
||||
notes: "",
|
||||
// Scopes this checkout to just this store's cart group — without this,
|
||||
// the backend checked out the entire cart regardless of which store's
|
||||
// "پرداخت" button was pressed.
|
||||
store_uuids: [group.store.uuid],
|
||||
});
|
||||
if (result.ok) {
|
||||
router.push(`/order-groups/${result.data.uuid}`);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
||||
|
|
@ -10,8 +11,21 @@ export interface CheckoutInput {
|
|||
scheduled_at: string | null;
|
||||
payment_method: PaymentMethod;
|
||||
notes: string;
|
||||
/**
|
||||
* Which store-cart-groups to check out, by store uuid. Required (not optional) —
|
||||
* every caller must be explicit about scope now; there's no more implicit
|
||||
* "everything in the cart" default. This is what fixes the bug where paying for
|
||||
* one store's order submitted every store's order: previously nothing on this
|
||||
* request identified which store(s) were intended, so the backend checked out
|
||||
* the whole cart every time.
|
||||
*/
|
||||
store_uuids: string[];
|
||||
}
|
||||
|
||||
export async function submitCheckout(input: CheckoutInput): Promise<ActionResult<OrderGroup>> {
|
||||
return callApi(() => winofyFetch<OrderGroup>("/v1/checkout/", { method: "POST", body: input }));
|
||||
const result = await callApi(() => winofyFetch<OrderGroup>("/v1/checkout/", { method: "POST", body: input }));
|
||||
// Only the scoped store(s)' items are removed server-side now — the cart page
|
||||
// (and the sticky-cart-bar badge) needs a fresh fetch to stop showing them.
|
||||
if (result.ok) revalidatePath("/cart");
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue