- 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>
31 lines
1.0 KiB
TypeScript
31 lines
1.0 KiB
TypeScript
export interface ParsedLineItem {
|
|
description: string;
|
|
hours: number | null;
|
|
rate: number | null;
|
|
}
|
|
|
|
export function parseLineItem(input: string): ParsedLineItem {
|
|
let text = input.trim();
|
|
let hours: number | null = null;
|
|
let rate: number | null = null;
|
|
|
|
// Extract hours: "3h", "3hr", "3hrs", "3 hours", "3.5hours"
|
|
const hoursMatch = /(\d+\.?\d*)\s*h(?:ours?|rs?)\b/i.exec(text);
|
|
if (hoursMatch?.[0] && hoursMatch[1]) {
|
|
hours = parseFloat(hoursMatch[1]);
|
|
text = text.replace(hoursMatch[0], " ").trim();
|
|
}
|
|
|
|
// Extract rate: "@120", "@$120", "at 120", "at $120", "$120/hr", "$120ph"
|
|
const rateMatch = /(?:@\s*\$?|at\s+\$?)(\d+\.?\d*)|(\$\d+\.?\d*)(?:\/h(?:rs?)?|ph)?\b/i.exec(text);
|
|
if (rateMatch?.[0]) {
|
|
const rawRate = rateMatch[1] ?? rateMatch[2] ?? "";
|
|
rate = parseFloat(rawRate.replace("$", ""));
|
|
text = text.replace(rateMatch[0], " ").trim();
|
|
}
|
|
|
|
const description = text.replace(/\s+/g, " ").replace(/^[\s,]+|[\s,]+$/g, "").trim();
|
|
|
|
return { description: description || input.trim(), hours, rate };
|
|
}
|