- Add `user_edited` field to QuoteItem type to gate AI rewrites on user-edited content - Add idempotent migration script to add and backfill the `user_edited` field across quote items - Replace plain Select components with searchable SupplierSearch combobox across order and quote pages - Add working days calculation utility and aged quote warning badges for overdue quotes - Implement expandable row previews in the quotes list view - Add full AI rewrite workflow: backend API endpoint, frontend modal, and n8n workflow definition - Update package scripts for the new migration
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
/**
|
||
* Working-days date math for the Quotes dashboard.
|
||
*
|
||
* "Working days" = weekdays only. Saturdays and Sundays are skipped.
|
||
* Public holidays are intentionally NOT subtracted — the team can layer
|
||
* that on later if it becomes a problem; for the current "show me the
|
||
* aged quotes" indicator, weekday-only is the right baseline.
|
||
*
|
||
* All comparisons are done in the browser's local time. Quotes have
|
||
* `created` timestamps in ISO UTC; we convert to local-day boundaries
|
||
* before counting so "3 working days old" means "3 weekdays since the
|
||
* day this row was created, in the user's timezone".
|
||
*/
|
||
|
||
/** Strip a Date down to its local-day midnight. */
|
||
function startOfLocalDay(d: Date): Date {
|
||
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||
}
|
||
|
||
/**
|
||
* Number of working days (Mon–Fri) between `then` and `now`, exclusive
|
||
* of both endpoints' days. So a quote created today returns 0; a quote
|
||
* created yesterday on a weekday returns 1; a quote created Friday
|
||
* morning checked Monday morning returns 1 (Sat/Sun skipped).
|
||
*
|
||
* Returns 0 when `then` is in the future or on the same day as `now`.
|
||
*/
|
||
export function workingDaysBetween(then: Date | string, now: Date = new Date()): number {
|
||
const thenDate = typeof then === "string" ? new Date(then) : then;
|
||
if (Number.isNaN(thenDate.getTime())) return 0;
|
||
|
||
const startThen = startOfLocalDay(thenDate);
|
||
const startNow = startOfLocalDay(now);
|
||
if (startNow <= startThen) return 0;
|
||
|
||
let count = 0;
|
||
const cursor = new Date(startThen);
|
||
cursor.setDate(cursor.getDate() + 1); // start counting the day AFTER creation
|
||
while (cursor <= startNow) {
|
||
const dow = cursor.getDay(); // 0 = Sunday, 6 = Saturday
|
||
if (dow !== 0 && dow !== 6) count++;
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* True iff `then` is older than `days` working days before `now`.
|
||
* Convenience wrapper around `workingDaysBetween` for the age warning.
|
||
*/
|
||
export function isOlderThanWorkingDays(
|
||
then: Date | string,
|
||
days: number,
|
||
now: Date = new Date()
|
||
): boolean {
|
||
return workingDaysBetween(then, now) >= days;
|
||
}
|