feat(MED-105): update order details redirect and shown page

This commit is contained in:
2025-08-11 09:21:40 +03:00
parent 556d7bd321
commit d582e222ce
19 changed files with 185 additions and 105 deletions

View File

@@ -91,7 +91,7 @@ export async function processMontonioCallback(orderToken: string) {
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false }); const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder }); const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
await createOrder({ medusaOrder, orderedAnalysisElements }); const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
const { productTypes } = await listProductTypes(); const { productTypes } = await listProductTypes();
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === ANALYSIS_PACKAGES_TYPE_HANDLE);
@@ -122,7 +122,7 @@ export async function processMontonioCallback(orderToken: string) {
// Send order to Medipost (no await to avoid blocking) // Send order to Medipost (no await to avoid blocking)
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
return { success: true }; return { success: true, orderId };
} catch (error) { } catch (error) {
console.error("Failed to place order", error); console.error("Failed to place order", error);
throw new Error(`Failed to place order, message=${error}`); throw new Error(`Failed to place order, message=${error}`);

View File

@@ -29,8 +29,8 @@ export default function MontonioCallbackClient({ orderToken, error }: {
setHasProcessed(true); setHasProcessed(true);
try { try {
await processMontonioCallback(orderToken); const { orderId } = await processMontonioCallback(orderToken);
router.push('/home/order'); router.push(`/home/order/${orderId}/confirmed`);
} catch (error) { } catch (error) {
console.error("Failed to place order", error); console.error("Failed to place order", error);
router.push('/home/cart/montonio-callback/error'); router.push('/home/cart/montonio-callback/error');

View File

@@ -7,7 +7,6 @@ export default async function MontonioCallbackPage({ searchParams }: {
}) { }) {
const orderToken = (await searchParams)['order-token']; const orderToken = (await searchParams)['order-token'];
console.log('orderToken', orderToken);
if (!orderToken) { if (!orderToken) {
return <MontonioCallbackClient error="Order token is missing" />; return <MontonioCallbackClient error="Order token is missing" />;
} }

View File

@@ -1,13 +1,16 @@
import { notFound } from 'next/navigation'; import { redirect } from 'next/navigation';
import { PageBody, PageHeader } from '@kit/ui/page';
import { retrieveOrder } from '~/medusa/lib/data/orders';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server'; import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import OrderCompleted from '@/app/home/(user)/_components/order/order-completed';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { getOrder } from '~/lib/services/order.service';
type Props = { import { retrieveOrder } from '@lib/data/orders';
params: Promise<{ orderId: string }>; import pathsConfig from '~/config/paths.config';
}; import Divider from "@modules/common/components/divider"
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
import OrderItems from '@/app/home/(user)/_components/order/order-items';
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
import { Trans } from '@kit/ui/trans';
export async function generateMetadata() { export async function generateMetadata() {
const { t } = await createI18nServerInstance(); const { t } = await createI18nServerInstance();
@@ -17,15 +20,33 @@ export async function generateMetadata() {
}; };
} }
async function OrderConfirmedPage(props: Props) { async function OrderConfirmedPage(props: {
params: Promise<{ orderId: string }>;
}) {
const params = await props.params; const params = await props.params;
const order = await retrieveOrder(params.orderId).catch(() => null);
const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null);
if (!order) { if (!order) {
return notFound(); redirect(pathsConfig.app.myOrders);
} }
return <OrderCompleted order={order} />; const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
if (!medusaOrder) {
redirect(pathsConfig.app.myOrders);
}
return (
<PageBody>
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
<Divider />
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
<OrderDetails order={order} />
<Divider />
<OrderItems medusaOrder={medusaOrder} />
<CartTotals medusaOrder={medusaOrder} />
</div>
</PageBody>
);
} }
export default withI18n(OrderConfirmedPage); export default withI18n(OrderConfirmedPage);

View File

@@ -0,0 +1,52 @@
import { redirect } from 'next/navigation';
import { PageBody, PageHeader } from '@kit/ui/page';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getOrder } from '~/lib/services/order.service';
import { retrieveOrder } from '@lib/data/orders';
import pathsConfig from '~/config/paths.config';
import Divider from "@modules/common/components/divider"
import OrderDetails from '@/app/home/(user)/_components/order/order-details';
import OrderItems from '@/app/home/(user)/_components/order/order-items';
import CartTotals from '@/app/home/(user)/_components/order/cart-totals';
import { Trans } from '@kit/ui/trans';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('cart:order.title'),
};
}
async function OrderConfirmedPage(props: {
params: Promise<{ orderId: string }>;
}) {
const params = await props.params;
const order = await getOrder({ orderId: Number(params.orderId) }).catch(() => null);
if (!order) {
redirect(pathsConfig.app.myOrders);
}
const medusaOrder = await retrieveOrder(order.medusa_order_id).catch(() => null);
if (!medusaOrder) {
redirect(pathsConfig.app.myOrders);
}
return (
<PageBody>
<PageHeader title={<Trans i18nKey="cart:order.title" />} />
<Divider />
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
<OrderDetails order={order} />
<Divider />
<OrderItems medusaOrder={medusaOrder} />
<CartTotals medusaOrder={medusaOrder} />
</div>
</PageBody>
);
}
export default withI18n(OrderConfirmedPage);

View File

@@ -10,6 +10,7 @@ import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import OrdersTable from '../../_components/orders/orders-table'; import OrdersTable from '../../_components/orders/orders-table';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import type { IOrderLineItem } from '../../_components/orders/types'; import type { IOrderLineItem } from '../../_components/orders/types';
import { getOrders } from '~/lib/services/order.service';
export async function generateMetadata() { export async function generateMetadata() {
const { t } = await createI18nServerInstance(); const { t } = await createI18nServerInstance();
@@ -20,7 +21,8 @@ export async function generateMetadata() {
} }
async function OrdersPage() { async function OrdersPage() {
const orders = await listOrders().catch(() => null); const orders = await listOrders();
const localOrders = await getOrders();
const { productTypes } = await listProductTypes(); const { productTypes } = await listProductTypes();
if (!orders || !productTypes) { if (!orders || !productTypes) {
@@ -30,13 +32,38 @@ async function OrdersPage() {
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages'); const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
const analysisPackageOrders: IOrderLineItem[] = orders.flatMap(({ id, items, payment_status, fulfillment_status }) => items const analysisPackageOrders: IOrderLineItem[] = orders.flatMap(({ id, items, payment_status, fulfillment_status }) => items
?.filter((item) => item.product_type_id === analysisPackagesType?.id) ?.filter((item) => item.product_type_id === analysisPackagesType?.id)
.map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` })) .map((item) => {
const localOrder = localOrders.find((order) => order.medusa_order_id === id);
if (!localOrder) {
return null;
}
return {
item,
medusaOrderId: id,
orderId: localOrder?.id,
orderStatus: localOrder.status,
analysis_element_ids: localOrder.analysis_element_ids,
}
})
.filter((order) => order !== null)
|| []); || []);
const otherOrders: IOrderLineItem[] = orders const otherOrders: IOrderLineItem[] = orders
.filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id)) .filter(({ items }) => items?.some((item) => item.product_type_id !== analysisPackagesType?.id))
.flatMap(({ id, items, payment_status, fulfillment_status }) => items .flatMap(({ id, items, payment_status, fulfillment_status }) => items
?.map((item) => ({ item, orderId: id, orderStatus: `${payment_status}/${fulfillment_status}` })) ?.map((item) => {
const localOrder = localOrders.find((order) => order.medusa_order_id === id);
if (!localOrder) {
return null;
}
return {
item,
medusaOrderId: id,
orderId: localOrder.id,
orderStatus: localOrder.status,
}
})
.filter((order) => order !== null)
|| []); || []);
return ( return (

View File

@@ -6,8 +6,8 @@ import React from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
export default function CartTotals({ order }: { export default function CartTotals({ medusaOrder }: {
order: StoreOrder medusaOrder: StoreOrder
}) { }) {
const { i18n: { language } } = useTranslation() const { i18n: { language } } = useTranslation()
const { const {
@@ -17,7 +17,7 @@ export default function CartTotals({ order }: {
tax_total, tax_total,
discount_total, discount_total,
gift_card_total, gift_card_total,
} = order } = medusaOrder
return ( return (
<div> <div>

View File

@@ -1,27 +0,0 @@
import { Trans } from '@kit/ui/trans';
import { PageBody, PageHeader } from '@kit/ui/page';
import { StoreOrder } from "@medusajs/types"
import Divider from "@modules/common/components/divider"
import CartTotals from "./cart-totals"
import OrderDetails from "./order-details"
import OrderItems from "./order-items"
export default async function OrderCompleted({
order,
}: {
order: StoreOrder,
}) {
return (
<PageBody>
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
<Divider />
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4 gap-y-6">
<OrderDetails order={order} />
<Divider />
<OrderItems order={order} />
<CartTotals order={order} />
</div>
</PageBody>
)
}

View File

@@ -1,47 +1,21 @@
import { StoreOrder } from "@medusajs/types"
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { formatDate } from 'date-fns';
import { AnalysisOrder } from "~/lib/services/order.service";
export default function OrderDetails({ order, showStatus }: { export default function OrderDetails({ order }: {
order: StoreOrder order: AnalysisOrder
showStatus?: boolean
}) { }) {
const formatStatus = (str: string) => {
const formatted = str.split("_").join(" ")
return formatted.slice(0, 1).toUpperCase() + formatted.slice(1)
}
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<span> <span>
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "} <Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
<span> <span>
{new Date(order.created_at).toLocaleDateString()} {formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
</span> </span>
</span> </span>
<span className="text-ui-fg-interactive"> <span className="text-ui-fg-interactive">
<Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.display_id}</span> <Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.medusa_order_id}</span>
</span> </span>
{showStatus && (
<>
<span>
<Trans i18nKey="cart:orderConfirmed.orderStatus" />:{" "}
<span className="text-ui-fg-subtle">
{formatStatus(order.fulfillment_status)}
</span>
</span>
<span>
<Trans i18nKey="cart:orderConfirmed.paymentStatus" />:{" "}
<span
className="text-ui-fg-subtle "
data-testid="order-payment-status"
>
{formatStatus(order.payment_status)}
</span>
</span>
</>
)}
</div> </div>
) )
} }

View File

@@ -1,7 +1,7 @@
import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types" import { StoreCartLineItem, StoreOrderLineItem } from "@medusajs/types"
import { TableCell, TableRow } from "@kit/ui/table" import { TableCell, TableRow } from "@kit/ui/table"
import LineItemOptions from "@modules/common/components/line-item-options" // import LineItemOptions from "@modules/common/components/line-item-options"
import LineItemPrice from "@modules/common/components/line-item-price" import LineItemPrice from "@modules/common/components/line-item-price"
import LineItemUnitPrice from "@modules/common/components/line-item-unit-price" import LineItemUnitPrice from "@modules/common/components/line-item-unit-price"
@@ -9,6 +9,7 @@ export default function OrderItem({ item, currencyCode }: {
item: StoreCartLineItem | StoreOrderLineItem item: StoreCartLineItem | StoreOrderLineItem
currencyCode: string currencyCode: string
}) { }) {
const partnerLocationName = item.metadata?.partner_location_name;
return ( return (
<TableRow className="w-full" data-testid="product-row"> <TableRow className="w-full" data-testid="product-row">
{/* <TableCell className="px-6 w-24"> {/* <TableCell className="px-6 w-24">
@@ -22,9 +23,9 @@ export default function OrderItem({ item, currencyCode }: {
className="txt-medium-plus text-ui-fg-base" className="txt-medium-plus text-ui-fg-base"
data-testid="product-name" data-testid="product-name"
> >
{item.product_title}{` (${item.metadata?.partner_location_name ?? "-"})`} {item.product_title}{` ${partnerLocationName ? `(${partnerLocationName})` : ''}`}
</span> </span>
<LineItemOptions variant={item.variant} data-testid="product-variant" /> {/* <LineItemOptions variant={item.variant} data-testid="product-variant" /> */}
</TableCell> </TableCell>
<TableCell className="px-6"> <TableCell className="px-6">

View File

@@ -7,10 +7,10 @@ import OrderItem from "./order-item"
import { Heading } from "@kit/ui/heading" import { Heading } from "@kit/ui/heading"
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
export default function OrderItems({ order }: { export default function OrderItems({ medusaOrder }: {
order: StoreOrder medusaOrder: StoreOrder
}) { }) {
const items = order.items const items = medusaOrder.items
return ( return (
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
@@ -27,7 +27,7 @@ export default function OrderItems({ order }: {
<OrderItem <OrderItem
key={item.id} key={item.id}
item={item} item={item}
currencyCode={order.currency_code} currencyCode={medusaOrder.currency_code}
/> />
)) ))
: repeat(5).map((i) => <SkeletonLineItem key={i} />)} : repeat(5).map((i) => <SkeletonLineItem key={i} />)}

View File

@@ -6,6 +6,7 @@ import { Eye } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { formatDate } from "date-fns"; import { formatDate } from "date-fns";
import { IOrderLineItem } from "./types"; import { IOrderLineItem } from "./types";
import { Trans } from '@kit/ui/trans';
export default function OrdersItem({ orderItem }: { export default function OrdersItem({ orderItem }: {
orderItem: IOrderLineItem, orderItem: IOrderLineItem,
@@ -22,15 +23,13 @@ export default function OrdersItem({ orderItem }: {
{formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')} {formatDate(orderItem.item.created_at, 'dd.MM.yyyy HH:mm')}
</TableCell> </TableCell>
{orderItem.orderStatus && ( <TableCell className="px-6 whitespace-nowrap">
<TableCell className="px-6"> <Trans i18nKey={`orders:status.${orderItem.orderStatus}`} />
{orderItem.orderStatus} </TableCell>
</TableCell>
)}
<TableCell className="text-right px-6"> <TableCell className="text-right px-6">
<span className="flex gap-x-1 justify-end w-[60px]"> <span className="flex gap-x-1 justify-end w-[60px]">
<Link href={`/home/analysis-results`} className="flex items-center justify-between text-small-regular"> <Link href={`/home/order/${orderItem.orderId}`} className="flex items-center justify-between text-small-regular">
<button <button
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer" className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
> >

View File

@@ -9,8 +9,6 @@ import {
import OrdersItem from "./orders-item"; import OrdersItem from "./orders-item";
import { IOrderLineItem } from "./types"; import { IOrderLineItem } from "./types";
const IS_SHOWN_ORDER_STATUS = true as boolean;
export default function OrdersTable({ orderItems, title }: { export default function OrdersTable({ orderItems, title }: {
orderItems: IOrderLineItem[]; orderItems: IOrderLineItem[];
title: string; title: string;
@@ -29,10 +27,9 @@ export default function OrdersTable({ orderItems, title }: {
<TableHead className="px-6"> <TableHead className="px-6">
<Trans i18nKey="orders:table.createdAt" /> <Trans i18nKey="orders:table.createdAt" />
</TableHead> </TableHead>
{IS_SHOWN_ORDER_STATUS && ( <TableHead className="px-6">
<TableHead className="px-6"> <Trans i18nKey="orders:table.status" />
</TableHead> </TableHead>
)}
<TableHead className="px-6"> <TableHead className="px-6">
</TableHead> </TableHead>
</TableRow> </TableRow>

View File

@@ -2,6 +2,7 @@ import { StoreOrderLineItem } from "@medusajs/types";
export interface IOrderLineItem { export interface IOrderLineItem {
item: StoreOrderLineItem; item: StoreOrderLineItem;
orderId: string; medusaOrderId: string;
orderId: number;
orderStatus: string; orderStatus: string;
} }

View File

@@ -3,6 +3,8 @@ import type { Tables } from '@kit/supabase/database';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import type { StoreOrder } from '@medusajs/types'; import type { StoreOrder } from '@medusajs/types';
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
export async function createOrder({ export async function createOrder({
medusaOrder, medusaOrder,
orderedAnalysisElements, orderedAnalysisElements,
@@ -32,6 +34,8 @@ export async function createOrder({
if (orderResult.error || !orderResult.data?.id) { if (orderResult.error || !orderResult.data?.id) {
throw new Error(`Failed to create order, message=${orderResult.error}, data=${JSON.stringify(orderResult)}`); throw new Error(`Failed to create order, message=${orderResult.error}, data=${JSON.stringify(orderResult)}`);
} }
return orderResult.data.id;
} }
export async function updateOrder({ export async function updateOrder({
@@ -56,14 +60,22 @@ export async function updateOrder({
export async function getOrder({ export async function getOrder({
medusaOrderId, medusaOrderId,
orderId,
}: { }: {
medusaOrderId: string; medusaOrderId?: string;
orderId?: number;
}) { }) {
const query = getSupabaseServerAdminClient() const query = getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
.from('analysis_orders') .from('analysis_orders')
.select('*') .select('*')
.eq('medusa_order_id', medusaOrderId) if (medusaOrderId) {
query.eq('medusa_order_id', medusaOrderId);
} else if (orderId) {
query.eq('id', orderId);
} else {
throw new Error('Either medusaOrderId or orderId must be provided');
}
const { data: order } = await query.single().throwOnError(); const { data: order } = await query.single().throwOnError();
return order; return order;

View File

@@ -47,6 +47,9 @@
"error": "Failed to update location" "error": "Failed to update location"
} }
}, },
"order": {
"title": "Order"
},
"orderConfirmed": { "orderConfirmed": {
"title": "Order confirmed", "title": "Order confirmed",
"summary": "Summary", "summary": "Summary",

View File

@@ -4,6 +4,15 @@
"table": { "table": {
"analysisPackage": "Analysis package", "analysisPackage": "Analysis package",
"otherOrders": "Order", "otherOrders": "Order",
"createdAt": "Ordered at" "createdAt": "Ordered at",
"status": "Status"
},
"status": {
"QUEUED": "Waiting to send to lab",
"ON_HOLD": "Waiting for analysis results",
"PROCESSING": "In progress",
"COMPLETED": "Completed",
"REJECTED": "Rejected",
"CANCELLED": "Cancelled"
} }
} }

View File

@@ -48,6 +48,9 @@
"error": "Asukoha uuendamine ebaõnnestus" "error": "Asukoha uuendamine ebaõnnestus"
} }
}, },
"order": {
"title": "Tellimus"
},
"orderConfirmed": { "orderConfirmed": {
"title": "Tellimus on edukalt esitatud", "title": "Tellimus on edukalt esitatud",
"summary": "Teenused", "summary": "Teenused",

View File

@@ -4,6 +4,15 @@
"table": { "table": {
"analysisPackage": "Analüüsi pakett", "analysisPackage": "Analüüsi pakett",
"otherOrders": "Tellimus", "otherOrders": "Tellimus",
"createdAt": "Tellitud" "createdAt": "Tellitud",
"status": "Olek"
},
"status": {
"QUEUED": "Ootab saatekirja saatmist",
"ON_HOLD": "Ootab analüüsi tulemusi",
"PROCESSING": "Töötlemisel",
"COMPLETED": "Valmis",
"REJECTED": "Tühistatud",
"CANCELLED": "Tühistatud"
} }
} }