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.description} +
{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 && (