updates on the ui changes and changed into doormile

This commit is contained in:
2026-08-24 18:10:47 +05:30
parent dcf9770ada
commit aa42d9ee81
409 changed files with 49950 additions and 66532 deletions

145
src/lib/assistant/scan.js Normal file
View File

@@ -0,0 +1,145 @@
import { getBookingsPage } from '@/api/doormile';
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
/**
* Reading bookings for the assistant.
*
* `GET /admin/bookings` has no date, status or tenant filter and caps `pagesize`
* at 1000 server-side, so every question is answered by draining pages and
* filtering here. A single `getBookings(1, 1000)` silently under-reports the
* moment an account passes 1000 lifetime bookings, which is why nothing in the
* assistant calls it.
*
* Every scan returns `{ rows, truncated, scanned, pagesFetched, total }` rather
* than a bare array, and **`truncated` is not optional to handle**. A count
* built on a capped scan is a floor, not a total — run it through `countPhrase`,
* append `truncationNote` to the detail, and build the audit entry with
* `scanCall`, which reports an error rather than a green tick beside a partial
* number.
*/
const BULK_PAGESIZE = 1000;
/** 12k rows — generous, but bounded so one question cannot hammer the API. */
const MAX_PAGES = 12;
/**
* Row order is not documented. When page 1 comes back newest-first the scan can
* stop as soon as a page ends older than the window; otherwise it scans to the
* budget. Detected per call rather than assumed, so a backend change degrades to
* "scan everything" — slower, still correct — rather than to a wrong answer.
*/
const isDescendingByCreatedAt = (rows) => {
if (rows.length < 2) return false;
const first = parseDoormileTimestamp(rows[0].createdat);
const last = parseDoormileTimestamp(rows[rows.length - 1].createdat);
return first.isValid() && last.isValid() && first.valueOf() > last.valueOf();
};
const inRange = (booking, start, end) => {
const at = parseDoormileTimestamp(booking.createdat);
if (!at.isValid()) return false;
const day = at.format('YYYY-MM-DD');
return day >= start && day <= end;
};
/**
* A short-lived page cache.
*
* A multi-part question runs several intents, and a comparison runs two ranges —
* each would otherwise re-drain the same pages. Deliberately small and local:
* this is not a caching layer to settle on.
*/
const PAGE_CACHE_TTL_MS = 20_000;
const pageCache = new Map();
const getPageCached = (page) => {
const hit = pageCache.get(page);
if (hit && Date.now() - hit.at < PAGE_CACHE_TTL_MS) return hit.promise;
const promise = getBookingsPage(page, BULK_PAGESIZE).catch((err) => {
/* Never cache a failure — the next question should retry, not inherit it. */
pageCache.delete(page);
throw err;
});
pageCache.set(page, { at: Date.now(), promise });
return promise;
};
export const fetchBookingsInRange = async (start, end) => {
const firstPage = await getPageCached(1);
const total = firstPage.total;
const pageCount = Math.max(1, Math.ceil(total / BULK_PAGESIZE));
const budget = Math.min(pageCount, MAX_PAGES);
const collected = [...firstPage.rows];
const descending = isDescendingByCreatedAt(firstPage.rows);
/* Newest-first and this page already ends before the window opens → every
later page is older still, so there is nothing left to find. */
const pageEndsBeforeRange = (rows) => {
if (!descending || !rows.length) return false;
const oldest = parseDoormileTimestamp(rows[rows.length - 1].createdat);
return oldest.isValid() && oldest.format('YYYY-MM-DD') < start;
};
let stoppedEarly = pageEndsBeforeRange(firstPage.rows);
let lastPageFetched = 1;
for (let page = 2; page <= budget && !stoppedEarly; page += 1) {
const next = await getPageCached(page);
lastPageFetched = page;
if (!next.rows.length) {
stoppedEarly = true;
break;
}
collected.push(...next.rows);
stoppedEarly = pageEndsBeforeRange(next.rows);
}
return {
rows: collected.filter((booking) => inRange(booking, start, end)),
/* Truncated only if the budget ran out with pages still unread AND the scan
did not stop early because it had already passed the window. */
truncated: !stoppedEarly && pageCount > budget,
scanned: collected.length,
pagesFetched: lastPageFetched,
total,
};
};
export const fetchBookingsForDay = (day) => fetchBookingsInRange(day, day);
/**
* A full scan with no date window — for a per-order lookup, which has to look
* everywhere rather than inside a range. The sentinel bounds keep one code path:
* they can never trigger the early stop, so this always drains to the page
* budget and reports `truncated` honestly if the id could be further back.
*/
export const scanBookings = () => fetchBookingsInRange('0000-01-01', '9999-12-31');
/** A count over a truncated scan is a floor — say so. */
export const countPhrase = (scan, n) => `${scan.truncated ? 'At least ' : ''}${n}`;
export const truncationNote = (scan) =>
scan.truncated
? `\n\nScanned the most recent ${scan.scanned.toLocaleString('en-IN')} of ${scan.total.toLocaleString(
'en-IN'
)} bookings — this is a floor, not a complete count.`
: '';
/**
* The audit entry for a scan. Reports the real page count, and flags itself as
* an error when truncated so the sources strip cannot show a green "complete"
* beside a partial number.
*/
export const scanCall = (scan, note) => ({
name: 'getBookingsPage',
target: `/admin/bookings (${scan.pagesFetched} page${scan.pagesFetched === 1 ? '' : 's'} × ${BULK_PAGESIZE})`,
status: scan.truncated ? 'error' : 'complete',
errorMessage: scan.truncated ? `Scan capped at ${MAX_PAGES} pages; ${scan.total} bookings exist` : undefined,
stats: note,
});
/** A plain audit entry for a non-scan call. */
export const call = (name, target, stats) => ({ name, target, status: 'complete', stats });