kitchen/docs/archive/PLAN-search-feature.md
jtricerolph 8d688b459d Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)
FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 12:15:39 +00:00

30 KiB

Search Feature Implementation Plan

Overview

Add a new "Search" main menu dropdown with three search pages:

  1. Invoices Search - Search/filter invoices (optionally including line item content)
  2. Line Items Search - Consolidated unique line items with price change detection
  3. Unit/Portion Definitions Search - Search ProductDefinition records

Plus: Reusable price history system with:

  • Price change detection on new invoices
  • Acknowledgement system to mark price changes as reviewed
  • History modal with graphs for any line item

All pages share common patterns: live search, session persistence, date filtering, grouping, and links to source records.


Data Models

Existing Models

Invoice: id, invoice_number, invoice_date, total, net_total, supplier_id, vendor_name, status, category, document_type

LineItem: id, invoice_id, product_code, description, unit, quantity, unit_price, amount, pack_quantity, unit_size, unit_size_type, portions_per_unit

ProductDefinition: id, kitchen_id, supplier_id, product_code, description_pattern, pack_quantity, unit_size, unit_size_type, portions_per_unit, portion_description, source_invoice_id, updated_at

New Model: AcknowledgedPrice

File: backend/models/acknowledged_price.py (NEW)

Tracks when a user acknowledges a price change so it doesn't keep flagging.

class AcknowledgedPrice(Base):
    __tablename__ = "acknowledged_prices"

    id: Mapped[int] = mapped_column(primary_key=True)
    kitchen_id: Mapped[int] = mapped_column(ForeignKey("kitchens.id"))
    supplier_id: Mapped[int] = mapped_column(ForeignKey("suppliers.id"))

    # Product identification (same logic as line item consolidation)
    product_code: Mapped[str | None] = mapped_column(String(100), nullable=True)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)

    # The acknowledged price point
    acknowledged_price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    acknowledged_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    acknowledged_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))

    # Which invoice triggered this acknowledgement
    source_invoice_id: Mapped[int | None] = mapped_column(ForeignKey("invoices.id"))
    source_line_item_id: Mapped[int | None] = mapped_column(ForeignKey("line_items.id"))

    # Unique: one acknowledged price per product per supplier per kitchen
    __table_args__ = (
        UniqueConstraint('kitchen_id', 'supplier_id', 'product_code', 'description',
                        name='uix_acknowledged_price'),
    )

New Settings Fields

File: backend/models/settings.py

# Price change detection settings
price_change_lookback_days: Mapped[int] = mapped_column(Integer, default=30)
price_change_amber_threshold: Mapped[int] = mapped_column(Integer, default=10)  # %
price_change_red_threshold: Mapped[int] = mapped_column(Integer, default=20)    # %

Backend Changes

1. New Search API Router

File: backend/api/search.py (NEW)

router = APIRouter(prefix="/api/search", tags=["search"])

# ============ Invoice Search ============
@router.get("/invoices")
async def search_invoices(
    q: str = "",                    # Search term (invoice_number, vendor_name)
    include_line_items: bool = False,  # Also search line item product_code/description
    supplier_id: int | None = None,
    status: str | None = None,
    date_from: date | None = None,  # Default: 30 days ago
    date_to: date | None = None,    # Default: today
    group_by: str | None = None,    # "supplier", "month", or null
    limit: int = 100,
    offset: int = 0
) -> InvoiceSearchResponse

# ============ Line Items Search (Consolidated) ============
@router.get("/line-items")
async def search_line_items(
    q: str = "",                    # Search term (product_code, description)
    supplier_id: int | None = None,
    date_from: date | None = None,
    date_to: date | None = None,
    group_by: str | None = None,    # "supplier", "invoice", "month"
    limit: int = 100,
    offset: int = 0
) -> LineItemSearchResponse
# Returns DISTINCT line items by (product_code OR description + supplier)
# With: most_recent_price, price_change_status, total_qty, occurrence_count

# ============ Unit/Portion Definitions Search ============
@router.get("/definitions")
async def search_definitions(
    q: str = "",                    # Search term (product_code, description_pattern)
    supplier_id: int | None = None,
    has_portions: bool | None = None,  # Filter by portions_per_unit is set
    limit: int = 100,
    offset: int = 0
) -> DefinitionSearchResponse

# ============ Line Item History (Reusable) ============
@router.get("/line-items/history")
async def get_line_item_history(
    product_code: str | None = None,
    description: str | None = None,
    supplier_id: int,
    date_from: date | None = None,  # Default: 12 months ago
    date_to: date | None = None,
) -> LineItemHistoryResponse
# Returns price history, qty stats for a specific product

# ============ Price Acknowledgement ============
@router.post("/line-items/acknowledge-price")
async def acknowledge_price_change(
    product_code: str | None,
    description: str | None,
    supplier_id: int,
    new_price: Decimal,
    source_invoice_id: int | None = None,
    source_line_item_id: int | None = None,
) -> AcknowledgePriceResponse
# Creates/updates AcknowledgedPrice record

2. Price History Service (Reusable)

File: backend/services/price_history.py (NEW)

class PriceHistoryService:
    """Reusable service for price change detection - used by search AND invoice review"""

    async def get_price_status(
        self,
        db: AsyncSession,
        kitchen_id: int,
        supplier_id: int,
        product_code: str | None,
        description: str | None,
        current_price: Decimal,
        lookback_days: int = 30,
        amber_threshold: int = 10,
        red_threshold: int = 20,
    ) -> PriceStatus:
        """
        Returns price status for a line item:
        - "consistent": Price matches history (green tick)
        - "no_history": First time seeing this item (no icon)
        - "amber": Small price change within threshold
        - "red": Large price change above threshold
        - "acknowledged": Price was flagged but user acknowledged it
        """

    async def get_history(
        self,
        db: AsyncSession,
        kitchen_id: int,
        supplier_id: int,
        product_code: str | None,
        description: str | None,
        date_from: date,
        date_to: date,
    ) -> LineItemHistory:
        """
        Returns full history for a product:
        - price_history: list of {date, price, invoice_id, invoice_number}
        - total_occurrences: int
        - total_quantity: Decimal
        - avg_qty_per_invoice: Decimal
        - avg_qty_per_week: Decimal
        - avg_qty_per_month: Decimal
        """

    async def acknowledge_price(
        self,
        db: AsyncSession,
        kitchen_id: int,
        user_id: int,
        supplier_id: int,
        product_code: str | None,
        description: str | None,
        new_price: Decimal,
        source_invoice_id: int | None,
        source_line_item_id: int | None,
    ) -> AcknowledgedPrice:
        """Creates or updates acknowledged price record"""

3. Response Models

class InvoiceSearchItem(BaseModel):
    id: int
    invoice_number: str | None
    invoice_date: date | None
    total: Decimal | None
    net_total: Decimal | None
    supplier_id: int | None
    supplier_name: str | None
    vendor_name: str | None
    status: str
    document_type: str | None

class InvoiceSearchResponse(BaseModel):
    items: list[InvoiceSearchItem]
    total_count: int
    grouped_by: str | None
    groups: list[GroupSummary] | None  # If grouped: [{name, count, total}]

class LineItemSearchItem(BaseModel):
    product_code: str | None
    description: str | None
    supplier_id: int | None
    supplier_name: str | None
    unit: str | None
    # Price info
    most_recent_price: Decimal | None      # Latest unit_price
    earliest_price_in_period: Decimal | None
    price_change_percent: float | None     # % change from earliest to most recent
    price_change_status: str               # "consistent", "amber", "red", "no_history"
    # Quantity info
    total_quantity: Decimal | None
    occurrence_count: int
    # Links
    most_recent_invoice_id: int
    most_recent_invoice_number: str | None
    most_recent_date: date | None
    # Definition info
    has_definition: bool
    portions_per_unit: int | None

class LineItemSearchResponse(BaseModel):
    items: list[LineItemSearchItem]
    total_count: int
    grouped_by: str | None
    groups: list[GroupSummary] | None

class DefinitionSearchItem(BaseModel):
    id: int
    product_code: str | None
    description_pattern: str | None
    supplier_id: int | None
    supplier_name: str | None
    pack_quantity: int | None
    unit_size: Decimal | None
    unit_size_type: str | None
    portions_per_unit: int | None
    portion_description: str | None
    source_invoice_id: int | None
    source_invoice_number: str | None
    updated_at: datetime

class DefinitionSearchResponse(BaseModel):
    items: list[DefinitionSearchItem]
    total_count: int

class GroupSummary(BaseModel):
    name: str           # Group name (supplier name, month "Jan 2024", etc.)
    count: int          # Number of items in group
    total: Decimal | None  # Sum of totals if applicable

# ============ History Modal Response ============
class PriceHistoryPoint(BaseModel):
    date: date
    price: Decimal
    invoice_id: int
    invoice_number: str | None

class LineItemHistoryResponse(BaseModel):
    product_code: str | None
    description: str | None
    supplier_name: str | None
    # Price history for chart
    price_history: list[PriceHistoryPoint]
    # Stats for period
    total_occurrences: int
    total_quantity: Decimal
    avg_qty_per_invoice: Decimal
    avg_qty_per_week: Decimal
    avg_qty_per_month: Decimal
    # Current status
    current_price: Decimal | None
    price_change_status: str

4. Register Router

File: backend/main.py

from api import search
app.include_router(search.router)

5. Update Invoice Line Items Response

File: backend/api/invoices.py

Add price status to line items when fetching invoice details:

# In get_invoice endpoint, for each line item:
price_status = await price_history_service.get_price_status(
    db, kitchen_id, supplier_id, product_code, description, unit_price
)
# Return: price_status ("consistent", "amber", "red", "no_history", "acknowledged")

Frontend Changes

1. Add Search Dropdown to Navigation

File: frontend/src/App.tsx

Add "Search" dropdown similar to "Reports" dropdown:

// In Header component, add state:
const [searchOpen, setSearchOpen] = useState(false)

// Add showSearch check:
const showSearch = showNavItem('/search-invoices') ||
                   showNavItem('/search-line-items') ||
                   showNavItem('/search-definitions')

// Add dropdown after Invoices link:
{showSearch && (
  <div style={styles.dropdownContainer}
       onMouseEnter={() => setSearchOpen(true)}
       onMouseLeave={() => setSearchOpen(false)}>
    <span style={styles.navLink}>Search </span>
    {searchOpen && (
      <div style={styles.dropdown}>
        {showNavItem('/search-invoices') &&
          <a href="/search/invoices" style={styles.dropdownLink}>Invoices</a>}
        {showNavItem('/search-line-items') &&
          <a href="/search/line-items" style={styles.dropdownLink}>Line Items</a>}
        {showNavItem('/search-definitions') &&
          <a href="/search/definitions" style={styles.dropdownLink}>Unit/Portion Definitions</a>}
      </div>
    )}
  </div>
)}

Add routes:

<Route path="/search/invoices" element={...} />
<Route path="/search/line-items" element={...} />
<Route path="/search/definitions" element={...} />

2. Create Search Components

2a. SearchInvoices.tsx (NEW)

File: frontend/src/components/SearchInvoices.tsx

Features:

  • Text search input (debounced 300ms for live search)
  • ☑ Include line items checkbox - when enabled, also searches line item product_code/description
  • Supplier dropdown filter
  • Status dropdown filter
  • Date range (default: last 30 days)
  • Group by dropdown: None / Supplier / Month
  • Session storage persistence for all filters
  • Results table with columns: Invoice #, Supplier, Date, Net Total, Status
  • Invoice # links to /invoice/{id} (opens in new tab)
  • When grouped: collapsible sections with group headers showing count/total

Layout:

┌───────────────────────────────────────────────────────────────────────────┐
│ Search Invoices                                                            │
├───────────────────────────────────────────────────────────────────────────┤
│ [Search input...] [☑ Include line items] [Supplier ▼] [Status ▼] [Group ▼]│
│ From: [____] To: [____]                                                    │
├───────────────────────────────────────────────────────────────────────────┤
│ Invoice #    │ Supplier    │ Date       │ Net Total │ Status              │
│ INV-001 ↗    │ Brakes      │ 15/01/2026 │ £234.50   │ confirmed           │
│ INV-002 ↗    │ Brakes      │ 14/01/2026 │ £156.20   │ confirmed           │
└───────────────────────────────────────────────────────────────────────────┘

2b. SearchLineItems.tsx (NEW)

File: frontend/src/components/SearchLineItems.tsx

Features:

  • Text search input (debounced 300ms)
  • Supplier dropdown filter
  • Date range (default: last 30 days)
  • Group by dropdown: None / Supplier / Invoice / Month
  • Session storage persistence
  • Results show consolidated items with:
    • Product Code, Description, Supplier
    • Most Recent Price with price change indicator:
      • 🟢 Green tick: Price consistent with history
      • 🟡 Amber ?: Small change (≤ amber threshold %)
      • 🔴 Red !: Large change (> red threshold %)
      • No icon: First time seeing this item
    • 📊 Price History button: Opens history modal
    • Total Qty with 📦 Qty History button: Opens history modal
    • Occurrences

    • Most Recent Invoice # (link to invoice)
    • Portions icon if definition exists

Consolidation Logic:

  • Group by: COALESCE(product_code, '') || '||' || COALESCE(description, '') || '||' || supplier_id
  • Show most recent values for each unique item
  • Aggregate: SUM(quantity), COUNT(*)
  • Compare earliest vs latest price in period for change detection

Layout:

┌─────────────────────────────────────────────────────────────────────────────────┐
│ Search Line Items                                                                │
├─────────────────────────────────────────────────────────────────────────────────┤
│ [Search...] [Supplier ▼] [From] [To] [Group ▼]                                  │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Code    │ Description      │ Supplier │ Price 📊    │ Qty 📦  │ # │ Invoice    │
│ ABC123  │ Chicken Breast   │ Brakes   │ £5.50 🟢    │ 45      │ 8 │ INV-001 ↗  │
│ XYZ789  │ Beef Mince 500g  │ Brakes   │ £6.20 🔴!   │ 30      │ 5 │ INV-003 ↗  │
│ -       │ Mixed Salad Bag  │ Booker   │ £3.20 🟡?   │ 120     │12 │ INV-015 ↗  │
└─────────────────────────────────────────────────────────────────────────────────┘

2c. SearchDefinitions.tsx (NEW)

File: frontend/src/components/SearchDefinitions.tsx

Features:

  • Text search input (debounced 300ms)
  • Supplier dropdown filter
  • "Has Portions Defined" checkbox filter
  • Session storage persistence
  • Results show:
    • Product Code, Description Pattern, Supplier
    • Pack Info (e.g., "120x15g")
    • Portions per Unit
    • Last Updated, Source Invoice (link)

Layout:

┌─────────────────────────────────────────────────────────────────────────────┐
│ Search Unit/Portion Definitions                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Search...] [Supplier ▼] [☑ Has Portions Defined]                           │
├─────────────────────────────────────────────────────────────────────────────┤
│ Code    │ Description      │ Supplier │ Pack      │ Portions │ Source       │
│ ABC123  │ Chicken Breast   │ Brakes   │ 4x2.5kg   │ 40       │ INV-001 ↗    │
│ XYZ789  │ Orange Juice 1L  │ Booker   │ 12x1L     │ 48       │ INV-022 ↗    │
└─────────────────────────────────────────────────────────────────────────────┘

3. Shared Search Utilities

File: frontend/src/utils/searchHelpers.ts (NEW)

// Session storage keys
export const SEARCH_STORAGE_KEYS = {
  invoices: {
    query: 'search-invoices-query',
    includeLineItems: 'search-invoices-include-line-items',
    supplier: 'search-invoices-supplier',
    status: 'search-invoices-status',
    dateFrom: 'search-invoices-from',
    dateTo: 'search-invoices-to',
    groupBy: 'search-invoices-group',
  },
  lineItems: { ... },
  definitions: { ... },
}

// Debounce hook for live search
export function useDebounce<T>(value: T, delay: number): T

// Date helpers
export function getDefaultDateRange(): { from: string, to: string }
export function formatDateForDisplay(date: string): string

4. Line Item History Modal (Reusable)

File: frontend/src/components/LineItemHistoryModal.tsx (NEW)

A reusable modal component used by:

  • SearchLineItems.tsx (📊 and 📦 buttons)
  • Review.tsx (price indicator clicks)

Props:

interface LineItemHistoryModalProps {
  isOpen: boolean
  onClose: () => void
  productCode: string | null
  description: string | null
  supplierId: number
  supplierName: string
  currentPrice?: Decimal  // For highlighting current price in chart
  onAcknowledge?: (newPrice: Decimal) => void  // Callback when price acknowledged
}

Layout:

┌─────────────────────────────────────────────────────────────────┐
│ Price History: Chicken Breast (ABC123)                      [X] │
│ Supplier: Brakes                                                │
├─────────────────────────────────────────────────────────────────┤
│ Date Range: [From: ____] [To: ____] (default: last 12 months)  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   📈 Price History Chart (Line chart with dates on X axis)      │
│      £6.00 ─────────────────────•                               │
│      £5.50 ────•────────────────┘                               │
│      £5.00 ────┘                                                │
│           Jan   Feb   Mar   Apr   May                           │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│ Stats for Period:                                               │
│ • Total Occurrences: 12                                        │
│ • Total Quantity: 45                                           │
│ • Avg Qty per Invoice: 3.75                                    │
│ • Avg Qty per Week: 1.2                                        │
│ • Avg Qty per Month: 5.0                                       │
├─────────────────────────────────────────────────────────────────┤
│ Current Price: £6.00 (🔴 +9% from previous £5.50)              │
│                                                                 │
│ [Acknowledge Price Change]  ← Only shown if price flagged      │
└─────────────────────────────────────────────────────────────────┘

Chart Library: Use recharts (lightweight, React-native)

5. Invoice Review Price Indicators

File: frontend/src/components/Review.tsx (MODIFY)

Add price status indicators next to each line item's unit price:

Per Line Item:

  • Fetch price_status from backend (included in line item response)
  • Display icon next to unit_price:
    • 🟢 ✓ (green): Price consistent with history
    • 🟡 ? (amber): Small change ≤ threshold
    • 🔴 ! (red): Large change > threshold
    • No icon: No history (first purchase)
  • Clicking icon opens LineItemHistoryModal
  • In modal, "Acknowledge Price Change" button updates AcknowledgedPrice record

Layout Change in Line Items Table:

│ Code    │ Description      │ Qty │ Unit Price      │ Amount │
│ ABC123  │ Chicken Breast   │ 4   │ £5.50 🟢        │ £22.00 │
│ XYZ789  │ Beef Mince 500g  │ 2   │ £6.20 🔴! [📊] │ £12.40 │
                                      ↑ Click to see history

6. Search Settings Section

File: frontend/src/pages/Settings.tsx (MODIFY)

Add new "Search Settings" section in Settings page:

┌─────────────────────────────────────────────────┐
│ Search Settings                                  │
├─────────────────────────────────────────────────┤
│ Price Change Detection                          │
│                                                 │
│ Lookback Period: [30] days                      │
│ (How far back to compare prices)               │
│                                                 │
│ Amber Threshold: [10] %                         │
│ (Highlight as warning if change ≤ this)        │
│                                                 │
│ Red Threshold: [20] %                           │
│ (Highlight as alert if change > amber)         │
│                                                 │
│ [Save]                                          │
└─────────────────────────────────────────────────┘

Files to Create/Modify

File Change
Backend
backend/models/acknowledged_price.py NEW - AcknowledgedPrice model
backend/models/settings.py Add price change threshold settings
backend/services/price_history.py NEW - Reusable price history service
backend/api/search.py NEW - Search endpoints + history + acknowledge
backend/api/invoices.py Add price_status to line items response
backend/main.py Register search router, run migration
backend/migrations/add_price_settings.py NEW - Migration for new settings + acknowledged_prices table
Frontend
frontend/package.json Add recharts dependency
frontend/src/App.tsx Add Search dropdown + routes
frontend/src/utils/searchHelpers.ts NEW - Debounce hook, session storage keys
frontend/src/components/SearchInvoices.tsx NEW - Invoice search page
frontend/src/components/SearchLineItems.tsx NEW - Line items search with price flags
frontend/src/components/SearchDefinitions.tsx NEW - Definitions search page
frontend/src/components/LineItemHistoryModal.tsx NEW - Reusable history modal with chart
frontend/src/components/Review.tsx Add price status icons to line items
frontend/src/pages/Settings.tsx Add Search Settings section + Access Control paths

Implementation Steps

Phase 1: Backend Foundation

  1. Create backend/models/acknowledged_price.py
  2. Add price change settings to backend/models/settings.py
  3. Create migration backend/migrations/add_price_settings.py
  4. Create backend/services/price_history.py service
  5. Create backend/api/search.py with all endpoints
  6. Update backend/api/invoices.py to include price_status
  7. Register router in backend/main.py

Phase 2: Frontend Search Pages

  1. Install recharts: npm install recharts
  2. Create frontend/src/utils/searchHelpers.ts
  3. Create SearchInvoices.tsx
  4. Create SearchLineItems.tsx
  5. Create SearchDefinitions.tsx

Phase 3: History Modal & Review Integration

  1. Create LineItemHistoryModal.tsx with chart
  2. Update Review.tsx with price status icons
  3. Add "Search Settings" section to Settings.tsx
  4. Add Search access control paths to Settings.tsx
  5. Update App.tsx with Search dropdown and routes

Phase 4: Test & Verify

  1. Rebuild containers
  2. Test all search pages
  3. Test price change detection on invoice review
  4. Test acknowledgement flow

Access Control Paths

Add to Settings Access Control checkboxes:

  • /search-invoices - Invoice Search
  • /search-line-items - Line Items Search
  • /search-definitions - Unit/Portion Definitions Search

Session Storage Keys

All search state persists during browser session:

Invoices:

  • search-invoices-query, search-invoices-supplier, search-invoices-status
  • search-invoices-from, search-invoices-to, search-invoices-group

Line Items:

  • search-line-items-query, search-line-items-supplier
  • search-line-items-from, search-line-items-to, search-line-items-group

Definitions:

  • search-definitions-query, search-definitions-supplier, search-definitions-has-portions

Live Search Implementation

Use debounced input (300ms delay) with react-query:

const [searchInput, setSearchInput] = useState('')
const debouncedSearch = useDebounce(searchInput, 300)

const { data, isLoading } = useQuery({
  queryKey: ['search-invoices', debouncedSearch, supplier, status, dateFrom, dateTo, groupBy],
  queryFn: () => fetchSearchResults(...)
})

Filter changes trigger immediate re-query (no debounce needed for dropdowns).


Verification

Search Pages

  1. Navigation: Search dropdown appears between Invoices and Reports
  2. Invoice Search:
    • Type in search box, results filter after 300ms
    • "Include line items" checkbox finds invoices by product names
    • Filter by supplier/status works
    • Date range defaults to last 30 days
    • Group by Supplier shows collapsible sections
    • Invoice # links open invoice in new tab
  3. Line Items Search:
    • Consolidated view shows unique items (not duplicates)
    • Shows most recent price with change indicator (🟢/🟡/🔴)
    • 📊 button opens history modal with price chart
    • 📦 button opens history modal with qty stats
    • Links to most recent invoice
  4. Definitions Search:
    • Shows all ProductDefinition records
    • "Has Portions" filter works
    • Links to source invoice

Price Change Detection

  1. Invoice Review Page:
    • Line items show price status icon next to unit price
    • 🟢 = consistent, 🟡 = small change, 🔴 = large change
    • Clicking icon opens history modal
    • "Acknowledge Price Change" button marks price as reviewed
    • After acknowledgement, icon changes to 🟢 on future invoices

History Modal

  1. Price History Chart:
    • Line chart shows price over time
    • Default range: last 12 months
    • Date range picker works
  2. Stats Display:
    • Total occurrences, total qty
    • Avg qty per invoice/week/month

Settings

  1. Search Settings Section:
    • Lookback period (default 30 days)
    • Amber threshold % (default 10%)
    • Red threshold % (default 20%)
    • Changes apply to price detection

Session Persistence

  1. Enter search term, navigate away, come back - search term preserved
  2. Close tab, reopen - state cleared (session storage)

Access Control

  1. Restrict search pages via Settings → Access Control
  2. Non-admin users don't see restricted search options