A line the AI filled now shows up purple until a person checks it, so nothing the assistant priced is ever taken on trust. - order-assistant-review.ts: the four client helpers — a line is pending only while it is filled, unreviewed and still priced from the quote; a human save keeps "ai_quote" / "quote" / "catalogue" and turns anything else into "manual"; the review patch records who checked the line and whether the prices were accepted unchanged. - order-assistant-save.ts: the save-time merge, the order-side twin of quote-assistant-save.ts. The value the operator committed wins over the one the page was loaded with, so the one-click "use this cost" copy really does end the AI's provenance. - The Process page applies the purple row class, shows the bot marker with the quote it was filled from, the reason and the doubts, and routes every line save through the merge. - The Orders board shows an "AI x/y" badge: how many lines the AI filled, and how many still need a review. One query per page, and never fatal — the board has to render whether or not the assistant's columns exist yet. - AiFillMarker grows an "order" variant instead of assuming a POS item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
/**
|
|
* order-assistant-save — the save-time merge the order Process page uses
|
|
* so a rep's save of an AI-filled line records the review.
|
|
*
|
|
* The order-side twin of `quote-assistant-save.ts`. Client-safe (types +
|
|
* `order-assistant-review` only), so it can be unit-tested.
|
|
*/
|
|
import {
|
|
buildOrderAiReviewPatch,
|
|
isOrderAiFillPending,
|
|
nextOrderPricingSourceOnSave,
|
|
} from "@/lib/order-assistant-review";
|
|
import type { OrderItem } from "@/lib/types";
|
|
|
|
/** The bits of a line the save merge reads. */
|
|
export type OrderAiLine = Pick<OrderItem, "ai_fill" | "pricing_source">;
|
|
|
|
/**
|
|
* Returns `patch` plus the AI-review bookkeeping for `item`:
|
|
*
|
|
* - `pricing_source`: only touched when the patch already carries one.
|
|
* "ai_quote" / "quote" / "catalogue" survive the save (the line is
|
|
* still priced from the quote); anything else becomes "manual".
|
|
* The value the operator committed wins over the one the page was
|
|
* loaded with, so the one-click "use this cost" copy — which sets
|
|
* "quote" — really does end the AI's provenance.
|
|
* - `ai_fill`: the reviewed copy, only for a pending AI line.
|
|
*
|
|
* Never removes keys from `patch` and never mutates it.
|
|
*/
|
|
export function withOrderAiReview(
|
|
item: OrderAiLine | undefined,
|
|
patch: Record<string, unknown>,
|
|
saved: { cost_price: number; selling_price: number; supplier: string | null },
|
|
reviewer: { id?: string; name: string },
|
|
now: Date,
|
|
): Record<string, unknown> {
|
|
const out: Record<string, unknown> = { ...patch };
|
|
|
|
const committedSource =
|
|
typeof patch.pricing_source === "string" ? patch.pricing_source : item?.pricing_source;
|
|
if ("pricing_source" in patch && committedSource !== "catalogue") {
|
|
out.pricing_source = nextOrderPricingSourceOnSave(committedSource);
|
|
}
|
|
|
|
if (!item || !isOrderAiFillPending(item)) return out;
|
|
|
|
Object.assign(out, buildOrderAiReviewPatch(item, saved, reviewer, now));
|
|
return out;
|
|
}
|
|
|
|
/** `{ id, name }` for the signed-in PB user (`authStore.record`). */
|
|
export function orderReviewerFromAuthRecord(
|
|
record: { id?: string; name?: unknown; email?: unknown } | null | undefined,
|
|
): { id?: string; name: string } {
|
|
const name = typeof record?.name === "string" ? record.name : "";
|
|
const email = typeof record?.email === "string" ? record.email : "";
|
|
return { id: record?.id, name: name || email || "" };
|
|
}
|