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;
|
return;
|
||||||
}
|
}
|
||||||
startTransition(async () => {
|
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({
|
const result = await submitCheckout({
|
||||||
address_uuid: addressUuid,
|
address_uuid: addressUuid,
|
||||||
delivery_type: "express",
|
delivery_type: "express",
|
||||||
scheduled_at: null,
|
scheduled_at: null,
|
||||||
payment_method: method,
|
payment_method: method,
|
||||||
notes: "",
|
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) {
|
if (result.ok) {
|
||||||
router.push(`/order-groups/${result.data.uuid}`);
|
router.push(`/order-groups/${result.data.uuid}`);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
import { winofyFetch } from "./winofy";
|
import { winofyFetch } from "./winofy";
|
||||||
import { callApi, type ActionResult } from "./result";
|
import { callApi, type ActionResult } from "./result";
|
||||||
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
||||||
|
|
@ -10,8 +11,21 @@ export interface CheckoutInput {
|
||||||
scheduled_at: string | null;
|
scheduled_at: string | null;
|
||||||
payment_method: PaymentMethod;
|
payment_method: PaymentMethod;
|
||||||
notes: string;
|
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>> {
|
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