diff --git a/src/app/(app)/quotes/[id]/page.tsx b/src/app/(app)/quotes/[id]/page.tsx
index 9811a54..0b5ba88 100644
--- a/src/app/(app)/quotes/[id]/page.tsx
+++ b/src/app/(app)/quotes/[id]/page.tsx
@@ -103,6 +103,10 @@ import QuoteEmailCard from "@/components/quote-email-card";
import RewriteQuoteModal from "@/components/rewrite-quote-modal";
import { AiItemHistoryTrigger } from "@/components/ai-item-history-trigger";
+import { AiFillMarker } from "@/components/ai-fill-marker";
+import { AiFillQuoteButton } from "@/components/ai-fill-quote-button";
+import { aiRowClass } from "@/lib/quote-assistant-review";
+import { reviewerFromAuthRecord, withAiReview } from "@/lib/quote-assistant-save";
import type { Quote, QuoteItem, QuoteAttachment, ItemIntel } from "@/lib/types";
import { buildDefaultQuoteEmailBody } from "@/lib/server/quote-email-template";
@@ -1699,11 +1703,25 @@ export default function QuoteDetailPage({ params }: { params: Promise<{ id: stri
setPriceUpdateSaving(true);
try {
const pb = getPocketBase();
- await pb.collection("quote_items").update(itemId, {
- cost_price: cost,
- markup,
- selling_price: selling,
- });
+ const patch = { cost_price: cost, markup, selling_price: selling };
+ // A pending AI-filled line gets its review recorded (and is marked
+ // processed once priced); normal lines are saved exactly as before.
+ const current = items.find((i) => i.id === itemId);
+ const payload = current
+ ? withAiReview(
+ current,
+ patch,
+ {
+ cost_price: cost ?? 0,
+ selling_price: selling ?? 0,
+ supplier: current.supplier || null,
+ },
+ reviewerFromAuthRecord(pb.authStore.record),
+ new Date(),
+ { markProcessedWhenPriced: true },
+ )
+ : patch;
+ await pb.collection("quote_items").update(itemId, payload);
toast.success("Price updated");
setPriceUpdateItem(null);
fetchQuote();
@@ -2382,6 +2400,11 @@ export default function QuoteDetailPage({ params }: { params: Promise<{ id: stri
Open Full Quote
)}
+ {/* "Let AI fill this quote" — only while the quote is still
+ open for pricing. */}
+ {!quote.is_template && ["new", "follow_up", "claimed"].includes(quote.status) && (
+
+ )}
{renderTemplateActions()}
{
const pct = itemMarkupPercent(item);
return (
-
+
{item.quantity}
@@ -2732,7 +2756,7 @@ export default function QuoteDetailPage({ params }: { params: Promise<{ id: stri
{items.map((item) => {
const pct = itemMarkupPercent(item);
return (
-
+
{item.description}
+
{item.is_processed ? "Yes" : "No"}
diff --git a/src/app/(app)/quotes/[id]/process/page.tsx b/src/app/(app)/quotes/[id]/process/page.tsx
index 19ce100..d3de572 100644
--- a/src/app/(app)/quotes/[id]/process/page.tsx
+++ b/src/app/(app)/quotes/[id]/process/page.tsx
@@ -52,6 +52,10 @@ import { ProcessItemAttachments } from "@/components/quotes/process-item-attachm
import { ProcessItemCard } from "@/components/quotes/process-item-card";
import { QuoteNoteCard } from "@/components/quotes/process-quote-note-card";
import { AiItemHistoryTrigger } from "@/components/ai-item-history-trigger";
+import { AiFillMarker } from "@/components/ai-fill-marker";
+import { AiFillQuoteButton } from "@/components/ai-fill-quote-button";
+import { aiRowClass, isAiFillPending } from "@/lib/quote-assistant-review";
+import { reviewerFromAuthRecord, withAiReview } from "@/lib/quote-assistant-save";
import { QuotedBeforePanel } from "@/components/quoted-before-panel";
import type { Quote, QuoteItem, QuoteAttachment, Supplier, Customer } from "@/lib/types";
import { cn } from "@/lib/utils";
@@ -372,7 +376,9 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
sp > 0 && candidate.cost_price > 0
? Math.round(((sp / candidate.cost_price) - 1) * 100 * 100) / 100
: 0;
- const persistedSnapshot = {
+ const persistedSnapshot = withAiReview(
+ item,
+ {
description: candidate.description || item.description || "",
quote_reference: (item as any).quote_reference || "",
quantity: item.quantity || 1,
@@ -386,7 +392,12 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
supplier: supplierId,
matched_price_list_id: candidate.id,
pricing_notes: `Auto-applied from picker — ${candidate.why}`,
- };
+ },
+ { cost_price: candidate.cost_price, selling_price: sp, supplier: supplierId },
+ reviewerFromAuthRecord(pb.authStore.record),
+ new Date(),
+ { markProcessedWhenPriced: false },
+ );
await pb.collection("quote_items").update(itemId, persistedSnapshot);
@@ -614,7 +625,9 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
const markup = item.cost_price && item.selling_price
? Number((((item.selling_price - item.cost_price) / item.cost_price) * 100).toFixed(2))
: 0;
- const persistedSnapshot = {
+ const persistedSnapshot = withAiReview(
+ item,
+ {
description: item.description,
quote_reference: (item as any).quote_reference || "",
quantity: item.quantity || 1,
@@ -630,14 +643,26 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
// variant. "Catalogue" is reserved for matches driven by
// the Check-pricing button (and the future email-intake
// automation) — never flip back to it on a human save.
- pricing_source:
- item.pricing_source === "catalogue" ? "catalogue" : "manual",
+ // Human saves map via nextPricingSourceOnSave inside
+ // withAiReview: "catalogue" and "ai_pos" survive, anything
+ // else becomes "manual". A pending AI line also gets its
+ // review recorded (ai_fill.reviewed_at).
+ pricing_source: "manual",
is_processed: !!(item.cost_price && item.selling_price),
// Flips to true the first time a human saves the item — gates
// the AI rewriter (server refuses to rewrite items where
// user_edited is still false).
user_edited: true,
- };
+ },
+ {
+ cost_price: item.cost_price || 0,
+ selling_price: item.selling_price || 0,
+ supplier: item.supplier || null,
+ },
+ reviewerFromAuthRecord(pb.authStore.record),
+ new Date(),
+ { markProcessedWhenPriced: false },
+ );
// PB needs `null` for an unset relation; local state holds it
// as `""` (the QuoteItem type's `supplier: string`). Mirror
// them explicitly so the PATCH and the setState values track
@@ -665,7 +690,7 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
supplier: supplierForLocal,
is_processed: !!persistedSnapshot.is_processed,
user_edited: true,
- pricing_source: persistedSnapshot.pricing_source,
+ pricing_source: persistedSnapshot.pricing_source as string,
}
: it,
),
@@ -1175,6 +1200,7 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
{formatCurrency(grand_total)}
)}
+
{items.length > 0 && (