Commit graph

23 commits

Author SHA1 Message Date
Dave
663c018a70 feat(quotes): add "Include older" toggle for searching full quote history
- Introduced a new toggle in the quotes and archive pages to allow users to include older quotes beyond the default 90-day cutoff.
- Updated the search parameters in the quotes and archive APIs to handle the new `includeOlder` flag, enabling full history searches.
- Enhanced the user interface to reflect the current search scope, providing clear feedback on whether the search is limited to recent quotes or includes the full archive.
- Adjusted the quotes search service to support the new functionality, ensuring efficient retrieval of older quotes when requested.
- Added relevant tests to verify the correct behavior of the new search feature and its integration with existing functionality.
2026-08-20 10:18:44 +02:00
Dave
88047eee33 feat(quotes): add "Completed (7 days)" tab for recent quotes
- Introduced a new tab for quotes completed within the last 7 days, enhancing the user experience by allowing quick access to recent activity.
- Updated the quotes search service to filter results based on the new "completed_recent" status, ensuring efficient retrieval of relevant quotes.
- Enhanced the quotes service to compute the cutoff date for the recent quotes dynamically, maintaining consistency across the application.
- Added tests to verify the functionality of the new filter, ensuring accurate behavior in the quotes search pipeline.
2026-08-19 13:36:41 +02:00
Dave
50944a3526 feat(quotes): add operator-facing notes functionality to quotes
- Introduced a new `quote_notes` field to store operator-facing free-text notes, distinct from the existing `notes` field used for rejection audit logs.
- Updated the quote processing page to include a `QuoteNoteCard` for editing and displaying the operator notes, ensuring seamless integration into the PDF generation flow.
- Enhanced the PDF generation logic to render the operator notes in a blue callout below the totals, improving visibility for end-users.
- Implemented a new service function `setQuoteNotes` to handle the saving of notes, including trimming whitespace and handling non-string inputs defensively.
- Added unit tests for the `setQuoteNotes` function to ensure correct behavior and data integrity when updating notes.
- Updated relevant types and documentation to reflect the new functionality and its intended use cases.
2026-08-17 15:22:16 +02:00
Dave
cf87a370cf feat(quotes): enhance quotes.submit functionality and error handling
- Updated the `quotes.submit` tool to allow submission of quotes with unpriced items by introducing a `partial: true` flag, transitioning the quote to `partial_approval_pending` instead of `approval_pending`.
- Enhanced the response structure of `submitQuote` to include `previousStatus`, `submittedAt`, and `targetStatus`, providing better context for the agent.
- Improved error handling in the `sendQuoteEmail` function to surface detailed readiness issues when a PDF cannot be generated, allowing operators to address specific gaps in quote items.
- Regenerated API documentation in `docs/api-mcp-layer.md` to reflect these changes, ensuring accuracy and clarity in tool descriptions.
- Added comprehensive tests to validate the new functionality and error handling improvements.
2026-08-15 23:57:51 +02:00
Dave
151ae8d67f feat(quotes): introduce quotes.submit tool for approval transition
- Added a new `quotes.submit` tool that transitions a quote from `new` to `approval_pending` once all line items are priced. This tool ensures that only fully priced quotes can be submitted for approval, enhancing the integrity of the approval process.
- Updated the API documentation in `docs/api-mcp-layer.md` to include the new `quotes.submit` tool and its usage details.
- Enhanced the `quotes` status enum in `docs/agents/hermes-skill.md` to reflect the new transition and its implications.
- Regenerated documentation to ensure accuracy, increasing the total tool count from 126 to 127.
- Added comprehensive tests for the `submitQuote` function to cover various scenarios, including idempotency and error handling for unpriced items.
2026-08-15 19:01:52 +02:00
Dave
c4b53bcc4b feat(mcp): add walk-in quote creation and enhance customer filtering
- Introduced a new `quotes.create` tool for creating quotes from scratch without an inbound email, allowing operators to generate quotes directly from the dashboard.
- Updated `customers.list` to support filtering by representative using either a 15-char PB user ID or a case-insensitive name/email match.
- Enhanced API documentation in `docs/api-mcp-layer.md` to reflect the addition of the new quote creation tool and updated customer filtering capabilities.
- Regenerated documentation to ensure accuracy, increasing the total tool count from 125 to 126.
2026-08-15 11:50:48 +02:00
Dave
397df8e2b2 fix: correct quote expiry handling for PocketBase's 0 default
PocketBase returns 0 for unset number fields, which broke the original
null-coalescing check in effectiveValidityDays, leading to PDFs showing
"Valid until: today" for all new quotes created via email ingest.

Changes include:
- Update effectiveValidityDays to treat 0, negatives, null/undefined as falling back to 30-day default
- Add test cases for 0 and negative validity_days inputs
- Explicitly set validity_days to QUOTE_VALID_DAYS when creating new quotes
- Add one-time idempotent migration script to backfill legacy invalid validity values
- Refactor item intel popover patch logic to use PocketBase SDK for authenticated requests
- Add n8n workflow for AI-powered item insight matching
2026-08-11 08:32:17 +02:00
SJCoetz
63048e51c9 feat: add AI quote processing pipeline and admin settings UI
Add complete end-to-end AI-powered quote extraction workflow:
- Disable PB SDK auto-cancellation to support parallel batch requests
- Add required runtime dependencies for AI processing and document parsing
- Implement AI settings backend, operator UI, and REST API routes
- Add quote ingestion, insight population, and manual re-run AI endpoints
- Build service layers for quote items, AI settings, and structured item insights
- Fix attachment upload handling and quote item history blob parsing
- Add supporting migrations, smoke tests, and shared utility functions
- Export previously private helper functions for cross-service reuse
2026-08-09 16:17:29 +02:00
SJCoetz
12fa65a481 feat: add new order, invoice, and dashboard management tools
- Add server-side `/api/v1/dashboard` endpoint to reduce client-side payload and round trips
- Implement invoice upload and order-item SKU update APIs with complete test suites
- Refactor order and order-items list endpoints (including MCP tools) to support proper PocketBase filter and sort params
- Fix pagination validation for order-items route to clamp page ≥1 and perPage 1-500
- Add redirects in next.config.ts to map underscore collection paths to hyphenated API routes
- Improve buyer tasks page to avoid redundant tab count refetches when switching tabs
- Refactor collection confirm components with editable SKUs and updated accessibility labels
- Add cached admin PocketBase auth to minimize repeated authentication calls
- Add performance testing scripts for dashboard endpoint latency
- Update quotes list route to default to excluding archived counts
- Fix photo strip component text and labels for non-invoice documents
- Refactor sprint page to lazy load supplier/customer data for pricing modal
- Clean up unused imports in test files
- Add full test coverage for all new APIs, services, and components
2026-08-05 23:00:54 +02:00
SJCoetz
d79c433704 fix(quotes): import QuoteItem + return real array from dedup branch
Production deploy failed the TypeScript type-check (next build exited 1)
because commit 2acfd2d changed CreateQuoteFromEmailResult.items from
'number' to 'QuoteItem[]' without updating:

  1. The type import — QuoteItem was never added to the import from
     @/lib/types (line 6 only brought in Quote, Customer, Company).

  2. The dedup branch (line 1060-1076) — still passed itemsCount: number
     where the new shape requires QuoteItem[]. Now fetches the real rows
     via getFullList and sets items_count = items.length for back-compat.

The build log shows the error was visible right at 'Running TypeScript ...'
but the deployment platform's log scraper cut off the actual stderr,
making it look like an opaque DeploymentException. Reproduced locally
with 'npm run build' to confirm both errors.

Build now passes (no next build / tsc failures). All 1392 tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-05 16:18:31 +02:00
SJCoetz
2acfd2d346 fix(mcp): add quotes.update + preserve unit in create_from_email
Closes the two real gaps from the agent's gap analysis (2026-08-05):

1. **quotes.update MCP tool** — was missing entirely. The quote header
   has fields (company_letterhead, quote_date, quote_reference, etc.)
   that couldn't be patched via MCP, so the agent had to pass
   letterhead at PDF-render time and couldn't persist it on the record.
   Now exposed with a strict field whitelist mirroring the
   quote_items whitelist pattern: any field outside the allowed set
   (status, customer, company, quote_number, id, created, updated, …)
   errors with forbidden_field rather than silently writing to PB.

   New catalog entry + dispatch handler in src/app/api/mcp/route.ts.
   Whitelist constant QUOTE_PATCH_FIELDS colocated with the existing
   QUOTE_ITEM_PATCH_FIELDS for reviewer discoverability.

2. **create_from_email now preserves the unit field** — extracted_items
   from the AI's RFQ parsing carries {description, quantity, unit,
   notes} but the create loop was silently dropping unit. Also the
   service returned items: <count> which made the agent believe no
   items were created when in fact stub rows existed. Now returns
   the actual QuoteItem array (with a back-compat items_count field)
   and the unit value lands on each row.

The agent's third claim (PB admin auth unavailable) is a misdiagnosis
— WFDASH uses its own AGENT_API_KEY and all writes go through the
Next.js admin client. No code change needed for that.

All 1379 tests pass. quote_items.update path unaffected. The
quotes.update path mirrors the same pattern: whitelist + reject empty
patch + call pb.collection.update.

Co-authored-by: Orca <help@stably.ai>
2026-08-05 11:39:16 +02:00
SJCoetz
e132319910 fix(orders,quotes): null-safe sort comparator in archive functions
The archive sort comparator in both getOrdersArchiveData and
getQuotesArchiveData was:

  if (av === undefined) return 1;
  if (bv === undefined) return -1;
  return (av > bv ? 1 : -1) * sortDir;

PocketBase returns `null` (not `undefined`) for unset fields
like `updated`, `email_body`, etc. The strict equality check
missed nulls, and the `av > bv` comparison where one side is
null fails the production tsc check (TS strict mode rejects
`null > string`).

Production build failed with:

  ./src/lib/server/orders-service.ts:687:13
  Type error: 'av' is possibly 'null'.

Fix: use `== null` (which matches both null and undefined),
and use explicit > / < comparisons that return 0 on equality
rather than a fragile ternary that falls through.

Also fixed in the matching function in quotes-service.ts — the
same sort comparator was used in getQuotesArchiveData and would
fail the same way.

All 1331 tests pass; npm run build completes cleanly.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:36:50 +02:00
SJCoetz
3ea7c9c189 feat(quotes): move completed+all to dedicated /quotes/archive page
Parallel to the orders archive treatment. On the live PB the
'Completed' tab on /quotes held 7,849 of 8,466 quotes (93%) and
the 'All' tab was 8,454. Same shape as orders: a 4k-id IN clause
blowing past PB's filter cap, with 'Page 1 of 392' on the tab
that operators didn't actually use for active work.

This commit moves both buckets to a dedicated /quotes/archive
page so:
  - Main /quotes is fast (active workflow: new, claimed,
    pending_approval, cancelled, delivery_note, email_failed,
    problem, templates = 542 quotes vs 8,466)
  - Archive can be honest about pagination
  - The badge counts on the main page stop being dominated by
    'Completed (7,849)'

# Service layer

- getQuotesArchiveData(params) in src/lib/server/quotes-service.ts
  - view: 'completed' (default, 7,849) or 'all' (8,466)
  - Strategy: one getFullList on quotes (fields: id,status) to
    populate the matching set AND the header counts in a single
    walk, chunk by 100 ids (PB's URL/filter cap is between
    4.5-6.7 KB), fire parallel getFullList queries on quotes
    with deep expand, sort + paginate in memory
  - Search hits quote_number, quote_reference, rfq_number,
    company, customer

- /api/v1/quotes/archive thin route: page, perPage, search,
  sort, view

# Page

- /quotes/archive/page.tsx: card list with header counts
  (completed / all badges), view toggle (Completed | All),
  search input, pagination footer. Click a quote to open
  /quotes/[id] in detail.

# Main /quotes updates

- Completed + All removed from the tab strip
- Counts on the main page now exclude completed + all
- 'Archive' button in the page header takes the operator to
  /quotes/archive
- Safety useEffect: if activeTab is completed/all (e.g.
  stale URL or local state), reset to 'new'

# Tests

4 new tests in quotes-service.test.ts:
  - Default 'completed' view returns only completed quotes,
    counts header includes both
  - 'all' view returns every counted-status quote
  - Search by quote_number and quote_reference
  - Empty result when no quotes match

# Also: webview tweaks by another agent

- /webview?src=hermes label in sidebar
- Hermes URL whitelist updated
- Some header text removed (the probe status remains)

Full suite: 1331/1331 pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:25:13 +02:00
SJCoetz
292cf2468e fix(orders): remove broken fields param + show contact_name + REP label
# Regression: orders/quotes list lost customer/company/rep data

Last commit added a `fields` parameter to the orders and quotes
list queries to trim the wire payload. Measured on a stale test
result, looked like a 16x win. Actual: PB 0.26's `fields`
parameter is a HARD ALLOWLIST of MAIN record columns only \u2014
specifying it makes PB drop the entire `expand` payload from
the response. Result: customer, company, rep, quote names
disappeared from every order/quote card.

Verified against the live PB:
  - With `fields`: expand is dropped, just relation IDs returned
  - Without `fields`: expand works, full records (55-57 KB for
    20 rows with deep expand)

Fix: remove the `fields` parameter from both list queries. The
slim-payload intent has to be paid at the schema level (cap the
unbounded text columns) rather than via PB query params.

The earlier '12x smaller' / '16x smaller' benchmarks were
measurements of the BROKEN response, not the working one. Real
wire sizes are unchanged from before the perf work \u2014 but the
other wins (server-side tab filter, combined status counts,
targeted users fetch, 60s counts cache) all still apply.

# Bonus: show contact_name on the order card

~76 of ~4.5k orders have `contact_name` set (the free-text
person the order is for, captured during email-intake when the
customer record is just the company). Added to the OrderCard
header between customer and rep, with a docstring explaining
the distinction from the customer relation and email_sender.

Added contact_name to the Order type and pinned the regression
with a test that asserts the orders list query does NOT pass
`fields` (so a future 'let's slim the payload' attempt
doesn't reintroduce the bug).

# REP label

The rep column on the order card now reads 'REP' (uppercase)
instead of 'rep' (lowercase). All 1312 tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 19:27:02 +02:00
SJCoetz
db1e7d6f76 perf(quotes): server-side tab filter, slim expand, combined counts + bonus bugfix
Same shape as the orders refactor \u2014 applied to the quotes page,
with a fix for a long-standing tab-count bug along the way.

# Measured against the live PB (8,466 quotes, 4,514 orders, 25,612 items)

Before:
  - 22 parallel per-status getList(1, 1) queries for the badge
    counts: 2.6s wall time (no quotes.status index)
  - 64 KB JSON response for 20 quotes with full deep-expand
    (45+ fields per quote, including unbounded email_body)
  - Page footer shows 'Page 1 of N' where N is the global count
    \u2014 'Completed' tab had 7,849 quotes = 'Page 1 of 393'

After (no PB index, just code):
  - 1 combined counts query: 138ms (19x faster)
  - 5.5 KB JSON response (12x smaller)
  - Server-side tab filter: page 1 of N reflects the tab's set
  - Cancelled tab badge now correctly counts rejected+cancelled

# Server-side read model

- getQuotesDashboardData in src/lib/server/quotes-service.ts
  - tab-scoped list + global counts in one round-trip
  - tab param drives a server-side filter (status IN (...) for
    workflow tabs, relation filter for delivery_note, is_template
    for templates, excludeTemplate for 'all')
  - pagination reflects the active tab's matching set, not the
    global ~8.5k quote count

- getQuoteStatusTabCounts: one combined getFullList with all
  17 counted statuses ORed (fields: 'status'), deduped in memory.
  Replaces 22 separate per-status scans. Cached 60s on the server
  with bypassCache option for future realtime refetches.

- /api/v1/quotes/list thin route: validates tab against
  QUOTE_TAB_ORDER, threads tab + nocache params to the service.

# Slim wire payload

- fields param on the orders list: just the columns the page reads
  + per-expand limits (quote.quote_number doesn't need
  email_body, company.company doesn't need its address field, etc.)
- Dropped the customer.company 3-level expand (the page never read
  expand.customer.expand.company \u2014 only expand.company.company,
  which is a direct relation from quote)
- searchQuotesWithItems: same fields added, with
  fetchQuoteItemsForQuotes now accepting fields

# Bonus bug: 'Cancelled' tab badge was always empty

The old getQuoteStatusCounts mapped both 'cancelled' AND 'rejected'
to the 'completed' count, so the page's 'Cancelled' tab badge
showed 0 even when 60+ rejected quotes existed. The page's
tabFilters separately grouped them under 'cancelled', so a quote
that was rejected showed up in the list but not the count.

The new QUOTE_TAB_STATUSES in the service layer is the canonical
mapping (cancelled absorbs both statuses). Page constants and
service constants now derive from the same source \u2014 no more drift.

# Index migration

scripts/migrations/2026-08-04-add-quotes-status-index.ts. Idempotent.
The live PB has no quotes.status index today (verified by hitting
/api/collections/quotes). With the index, the combined counts
query drops from 138ms to sub-10ms on the live instance.

# Tests

6 new tests in quotes-service.test.ts:
  - QUOTE_TAB_STATUSES contract (cancelled absorbs rejected,
    no empty status lists unless special-cased)
  - getQuoteStatusTabCounts buckets correctly into tabs
  - Cache hit returns the same counts on second call
  - bypassCache: true sees new state
  - getQuotesDashboardData end-to-end: tab-scoped items +
    global counts
  - Regression guard for the cancelled/rejected bug

Mock infrastructure upgrade: the quotes-service mock's where
function is now a proper recursive-descent filter parser (handles
nested || inside parens, empty quoted values). getList mock now
returns totalPages.

Full suite: 1312/1312 pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 19:01:31 +02:00
SJCoetz
4444842ef5 feat: collection-map updates, todays-stops, quote/approval rework, buyer-tasks, e2e
- Add todays-stops test and component to collection-map
- Expand collection-map sidebar and pin-model
- Rework orders/[id]/process page
- Add quotes/[id]/page and process pages
- Add quotes/approvals/[id] page
- Add buyer-tasks generate-po route
- Add quotes partial-approve and pdf routes
- Add purchase-orders [id]/pdf route
- Add e2e create-flows spec
- Add superpowers plan doc
- Update pb_schema-2.json
- Update form components (company, customer, order, quote, supplier)
- Update UI components (button, command, dialog)
- Add collection-pool test components

Co-authored-by: Orca <help@stably.ai>
2026-08-03 09:33:45 +02:00
SJCoetz
7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00
SJCoetz
cdaa2e2b25 fix, feat, style: standardize quote numbers, polish UI
- Standardize quote number format from Q-YYYY-MM-NNN to QYYYYMMNNN across all client, server, test and API code
- Add atomic backfill logic for quote counters to avoid sequence collisions with existing records
- Rearrange Delivery Notes and Repairs to the Operations sidebar section
- Add supplier count badge to pricelist catalogue and improve filter tab overflow styling
- Optimize font loading in root layout to skip preloading unused font weights
- Improve table styling for customer and company pages with better text wrapping and empty states
- Fix rep display logic in quotes page to show "Unassigned" when no rep is assigned
- Add supporting documentation, PocketBase counter schemas and setup migration scripts
- Remove embedded SupplierMap from suppliers page for cleaner layout
- Improve invoice card layout for company and customer info spacing
- Improve filter label styling and accessibility in collection map page
2026-07-10 22:21:50 +02:00
SJCoetz
7f924ccc45 refactor: switch hardcoded UI colors to semantic theme tokens
Overhaul the app's color system by replacing all hardcoded Tailwind color classes with semantic theme variables to ensure consistent theming across all components, pages, and tests.

Additional changes:
- Add assigned date support for order item status updates: add `assignedDate` field to the order item status PATCH API, implement server-side normalization/validation, and add corresponding tests
- Refactor purchase order PDF workflows to use the consolidated `purchase_orders.documents` field instead of the legacy separate `purchase_order_pdfs` collection
- Fix quote number parsing and formatting to support both dashed (Q-YYYY-MM-NNN) and dashless (QYYYYMMNNN) formats
- Rename `driver_pool` item status to `collection_pool` across the codebase
- Add null handling for order item quantities and pricing to display "—" for missing values
- Improve filter tab styling: make mobile tabs scrollable, prevent text wrapping, hide scrollbar
- Add new utility scripts, API routes, migrations, and polish small UI/UX fixes including tsconfig updates and theme hook improvements
2026-07-07 17:22:03 +02:00
SJCoetz
6c07a91693 refactor: drop order-level status, shift to item-driven workflow
This commit overhauls the order management system to use item-level status tracking instead of a single order-level status:
- Remove deprecated `orders.status` field and add `invoiced` and quote match audit fields
- Add `isOrderComplete` utility with tests to validate fully delivered orders
- Add purchase order PDF rendering support and tooling
- Add geo coordinate fields for companies/customers with map link UI
- Add REST management APIs for customers, companies, and suppliers
- Update customer/company services to use relation fields instead of free text
- Add status pill and tab filter components for item status visualization
- Add quote-to-order matching logic and unit tests
- Update all pages, components, and tests to match the new schema
- Include all required PocketBase migrations and schema updates
2026-06-29 13:06:01 +02:00
SJCoetz
588e9762c2 feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 22:57:01 +02:00
SJCoetz
714e910949 feat: add branded quote PDF generation and in-app viewer
This commit adds full quote PDF generation functionality with per-company letterhead branding:
- Server-side PDF rendering using @react-pdf/renderer matching the sample quote layout
- In-app modal PDF viewer with print, download, and new-tab controls
- Letterhead service to fetch and apply company-specific branding (CNL/ISHERWOOD)
- ZAR currency and date formatting utilities for South African quotes
- Updated QuoteAttachment type to handle PocketBase's array-based file tokens
- Fixed PocketBase attachment URL generation in quote email card
- Removed deprecated `included_in_quote` field from QuoteItem and quotes service
- Updated quote item creation test to validate draft item logic
- Added comprehensive unit tests for PDF rendering and formatting helpers
- Added dev scripts for debugging PDF and image rendering issues
- Added sample generated PDF files for visual verification
- Updated tsconfig to exclude development scripts directory
- Added @react-pdf/renderer production dependency
- Removed JEI as a valid letterhead option per requirements
2026-06-23 08:30:38 +02:00
SJCoetz
41b5fc4cbf feat(quotes): add email-to-quote pipeline with attachments and replies 2026-06-16 20:46:28 +02:00