QuoteDetail
Presentation of a quote or quote request. It never fetches the record; the caller passes the resolved one.
import { QuoteDetail } from "@evolve-storefront/feature-quote/quote-detail";Renders inside QuoteProvider.
Design notes
Ordering happens here, on the quote — the thing the buyer is agreeing to is what they should be looking at while they agree to it. The affordance is gated on the ordering grant and on an orderable status inside the validity window, and a quote becomes an order directly rather than via a basket, the backend carrying the negotiated prices a cart would reprice away.
Paying does not happen here. Ordering produces an order, and an order is paid the way every other order is. This view once carried a payment picker; the whole surface is gone.
Preview
Usage
A pending quote inside its window. One control, and clicking it turns the quote into an order — there is no method to choose first, because ordering no longer takes a payment.
const { data: quote } = useQuote(quoteId);
<QuoteDetail quote={quote} kind="quote" // A node from your own client island, so this view and its parts stay // Server Components. Stories pin `now`; a page omits it. orderAction={<OrderQuoteButton quote={quote} />}/>;
// OrderQuoteButton is the island `orderAction` receives. It owns the mutation and// the routing, so nothing else on the page has to be a Client Component."use client";export function OrderQuoteButton({ quote }: { quote: Quote }) { const placeOrder = usePlaceQuoteOrder(); const router = useRouter(); return ( <QuoteOrderAction quote={quote} isPlacing={placeOrder.isPending} onOrder={() => placeOrder.mutate( { quote }, { onSuccess: ({ order }) => router.push(orderPaths(order.id).detail) }, ) } /> );}Props
| Prop | Type | Description |
|---|---|---|
quote | Quote | QuoteRequest | The resolved quote or quote request. The caller owns the query. |
kind | QuoteKind | Which kind — quotes offer the order action, requests don't. |
orderAction? | ReactNode | The control that turns this quote into an order, where the page offers one.
Rendered in the receipt under the total, on a MoreA node rather than a callback. Placing an order is the app's to wire, and a
function prop makes every caller a Client Component — Pass |
answerAction? | ReactNode | The control that replies to or declines this quote, where the page offers one:
|
now? | Date | "Now" for the validity check; defaults to the current time. |
renderItem? | QuoteLineItemRenderer<Quote | QuoteRequest["lineItems"][number]> | Replace the line rows in the default layout. Ignored when |
requestComment? | ReactNode | What the buyer wrote when they asked. A request passes nothing: its own
|
requestedBy? | string | Who asked, where the page knows. Not a field on the quote: the requester is the project's to resolve, from its own extension or its own directory. |
orderLink? | ReactNode | The order this quote became, as a link. A quote is either still orderable — the receipt offers it — or it has been ordered, and then the order is what the buyer came for; the two are never both present, so they share the slot under the total. MoreA node, for the same reason Rendered only where the quote has an order, so a page can hand one over unconditionally. |
headingLevel? | 3 | 1 | 2 | The heading level for the record's title. A detail page is usually 1. |
children? | ReactNode | Compose the pieces ( |
Any other props go to the root element, like on a plain HTML element.
Change the layout
Pass children to QuoteDetail to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-quote/quote-detail.
// Children replace the standard layout: only what you compose renders.<QuoteDetail quote={quote} kind="quote"> <QuoteDetailHeader entry={quote} kind="quote" /> <QuoteLineItems items={quote.lineItems} /> <QuoteAmountBreakdown quote={quote} /></QuoteDetail>;QuoteDetailLayoutWhat QuoteDetail renders when given no children, in the order detail's shape: the title, the facts, the negotiation (read before the basket, since a total €790 off means nothing until you have read what took it off), the delivery address, then the lines with a sticky receipt.
| Prop | Type | Description |
|---|---|---|
quote | Quote | QuoteRequest | The resolved quote or quote request. The caller owns the query. |
kind | QuoteKind | Which kind — quotes offer the order action, requests don't. |
now? | Date | "Now" for the validity check; defaults to the current time. |
orderAction? | ReactNode | The control that turns this quote into an order, where the page offers one.
Rendered in the receipt under the total, on a MoreA node rather than a callback. Placing an order is the app's to wire, and a
function prop makes every caller a Client Component — Pass |
answerAction? | ReactNode | The control that replies to or declines this quote, where the page offers one:
|
orderLink? | ReactNode | The order this quote became, as a link. A quote is either still orderable — the receipt offers it — or it has been ordered, and then the order is what the buyer came for; the two are never both present, so they share the slot under the total. MoreA node, for the same reason Rendered only where the quote has an order, so a page can hand one over unconditionally. |
renderItem? | QuoteLineItemRenderer<Quote | QuoteRequest["lineItems"][number]> | Replace the line rows in the default layout. Ignored when |
requestComment? | ReactNode | What the buyer wrote when they asked. A request passes nothing: its own
|
requestedBy? | string | Who asked, where the page knows. Not a field on the quote: the requester is the project's to resolve, from its own extension or its own directory. |
headingLevel? | 3 | 1 | 2 | The heading level for the record's title. A detail page is usually 1. |
QuoteAmountBreakdownA quote's amounts as an offer: the items at their regular price, the seller's discount, delivery, then net, VAT and the gross total.
The rows are net throughout and read the amounts rather than summing the lines, so
they satisfy subTotal − discountTotal + shippingCosts = total however the seller
priced. A reverse-charged or exported quote (vatOutcome) is taxed at 0% and gets a
noted zero VAT row.
| Prop | Type | Description |
|---|---|---|
quote | Quote | QuoteRequest |
Any other props go to the root element, like on a plain HTML element.
QuoteAnswerActionReply to a pending quote asking for a different offer, or decline it, each confirmed in place.
Shown under the same rule as ordering: the ordering grant and a quote that is pending inside its validity window. A refusal naming a status withdraws both.
| Prop | Type | Description |
|---|---|---|
quote | Quote | |
onReplied? | ((quote: Quote) => void) | Called with the quote as it stands after the reply was sent. |
onDeclined? | ((quote: Quote) => void) | Called with the declined quote. |
now? | Date | "Now" for the validity check; defaults to the current time. |
Any other props go to the root element, like on a plain HTML element.
QuoteDetailHeaderThe record's title, with the page's action beside it: "Quote #PO-8823".
| Prop | Type | Description |
|---|---|---|
entry | Pick<Quote | QuoteRequest, "purchaseOrderNumber"> | |
kind? | QuoteKind | Which kind, so the title says which record this is. |
action? | ReactNode | The page's action, top right. A node, so the header stays server-renderable. |
headingLevel? | 3 | 1 | 2 | The heading level for the record's title. A detail page is usually 1. |
Any other props go to the root element, like on a plain HTML element.
QuoteDetailInfoThe quote's facts as one table, as the order detail has them: the seller's quote number, when it was requested and by whom, until when it is valid, its status and how it is delivered.
| Prop | Type | Description |
|---|---|---|
entry | Quote | QuoteRequest | |
requestedBy? | string | Who asked, where the page knows. Not a field on the quote: the project resolves it. |
now? | Date | "Now" for the validity check; defaults to the current time. The same |
Any other props go to the root element, like on a plain HTML element.
QuoteLineItemsThe quote's priced line items.
| Prop | Type | Description |
|---|---|---|
items | QuoteLineItem[] | The quoted lines. Taking the items rather than the whole entry is what
carries an extended query's item type through — |
renderItem? | QuoteLineItemRenderer<QuoteLineItem> |
Any other props go to the root element, like on a plain HTML element.
QuoteMessagesThe negotiation, in the order it happened.
A quote is an exchange, and rendered as labelled fields it reads as metadata about a record. Aligned and tinted per author it reads as what it is, and the arrangement says who spoke before the label is read. That matters most on the one page it is missing from: a total that is €790 off means nothing until you have read what took it off, which is why this goes above the basket rather than under it.
Renders nothing when there is nothing to show, so a caller passes what it has rather than guarding.
| Prop | Type | Description |
|---|---|---|
ask? | ReactNode | What the buyer wrote when they asked — the message that opened this. MoreIt lives on the quote request, so a request passes its own |
answer? | ReactNode | What the seller said — |
reply? | ReactNode | What the buyer said about the offer that came back — |
buyerName? | string | Who the buyer is, where the page knows. Inside a company the useful answer is which colleague wrote it, and that is a name this package cannot look up — the requester is not a field on the quote. Defaults to a role label. |
Any other props go to the root element, like on a plain HTML element.
QuoteOrderActionTurn the quote into an order.
Renders nothing unless the buyer holds the ordering grant and the quote is orderable inside its validity window — a separate grant from requesting, because ordering is the acceptance. A refusal naming a status withdraws the control; a plain failure keeps it.
| Prop | Type | Description |
|---|---|---|
quote | Quote | |
onOrder | () => void | Turn the quote into an order. Wire it to |
isPlacing? | boolean | |
refusedStatus? | QuoteStatus | null | The status a refusal reported, when the attempt was refused because the quote is no longer orderable. Setting it withdraws the affordance: retrying cannot succeed, so only the reason is left on the page. |
hasFailed? | boolean | A refusal that named no status — a validation failure. The control stays, because retrying is exactly what the buyer should do. |
now? | Date | "Now" for the validity check; defaults to the current time. |
Any other props go to the root element, like on a plain HTML element.
QuoteOrderRefusalWhy the attempt was refused.
Keyed on the status the refusal reported, never on its message, which is
developer-facing and unlocalized. A refusal with no status gets the generic retry,
because a validation failure says nothing about the quote itself.
| Prop | Type | Description |
|---|---|---|
status? | QuoteStatus | null | The status a refusal reported. It can disagree with the quote's own status and
the refusal is the authority: the server compares the validity date at
conversion, so a quote reading |
hasFailed? | boolean | A refusal that named no status — a validation failure. |
Any other props go to the root element, like on a plain HTML element.
QuoteStatusBadgeQuote / request status badge, labelled from the Quote.status namespace.
Pass validUntil (and optionally a fixed now) and it also shows EXPIRED for a
PENDING quote past its date, which the server only reports on the next read.
| Prop | Type | Description |
|---|---|---|
status | "PENDING" | "ACCEPTED" | "DECLINED" | "RENEGOTIATION_REQUESTED" | "EXPIRED" | "CANCELLED" | "SUBMITTED" | "REJECTED" | |
data-component? | undefined | A badge's identity and its published axes are not a caller's to rename. Typed away so a call site renaming one fails to compile rather than silently shadowing the selector a brand was told to use. |
data-tone? | undefined | |
data-status? | undefined | |
validUntil? | string | null | A quote's validity deadline; expiry is derived from it. |
now? | Date |
Any other props go to the root element, like on a plain HTML element.
Loading and empty states
Render these while the data loads or when there is nothing to show, so the page does not jump when it arrives.
| Loading | QuoteDetailSkeleton | Loading state for QuoteDetail — the same flex flex-col gap-8 outer section. |

