Archived
feat: add fuzzy autocomplete and NL quick-add for invoice line items
- Description field now shows a fuzzy-matched dropdown of past line items (description, hours, rate) as you type via Fuse.js — zero server cost - Selecting a suggestion pre-fills description, hours, and rate in one click - NL quick-add bar lets you type e.g. "3hrs web design @120" + Enter to append a fully-parsed line item without clicking through fields - New tRPC query `getLineItemHistory` returns deduplicated past line items for the current user, ordered by recency - New `parseLineItem` utility handles hours/rate extraction via regex Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { desc, eq, inArray } from "drizzle-orm";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
invoices,
|
||||
@@ -131,6 +131,40 @@ export const invoicesRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
getLineItemHistory: protectedProcedure.query(async ({ ctx }) => {
|
||||
const userInvoices = await ctx.db
|
||||
.select({ id: invoices.id })
|
||||
.from(invoices)
|
||||
.where(eq(invoices.createdById, ctx.session.user.id));
|
||||
|
||||
if (userInvoices.length === 0) return [];
|
||||
|
||||
const invoiceIds = userInvoices.map((i) => i.id);
|
||||
const rows = await ctx.db
|
||||
.select({
|
||||
description: invoiceItems.description,
|
||||
hours: invoiceItems.hours,
|
||||
rate: invoiceItems.rate,
|
||||
createdAt: invoiceItems.createdAt,
|
||||
})
|
||||
.from(invoiceItems)
|
||||
.where(inArray(invoiceItems.invoiceId, invoiceIds))
|
||||
.orderBy(desc(invoiceItems.createdAt))
|
||||
.limit(500);
|
||||
|
||||
// Deduplicate by description, keeping most recent occurrence
|
||||
const seen = new Set<string>();
|
||||
return rows
|
||||
.filter((r) => {
|
||||
const key = r.description.toLowerCase();
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
})
|
||||
.slice(0, 200)
|
||||
.map(({ description, hours, rate }) => ({ description, hours, rate }));
|
||||
}),
|
||||
|
||||
getCurrentOpen: protectedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
// Get the most recent draft invoice
|
||||
|
||||
Reference in New Issue
Block a user