From 663c018a70d459fef32ea2cd6d44c1b02adc86fb Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 20 Aug 2026 10:18:44 +0200 Subject: [PATCH] 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. --- .../console-2026-08-20T06-41-57-440Z.log | 117 +++ .../console-2026-08-20T06-42-55-620Z.log | 100 +++ .../console-2026-08-20T06-43-42-418Z.log | 85 ++ .../page-2026-08-20T06-42-01-523Z.yml | 3 + .../page-2026-08-20T06-42-34-924Z.yml | 3 + .../page-2026-08-20T06-42-58-756Z.yml | 2 + .../page-2026-08-20T06-43-00-617Z.yml | 3 + .../page-2026-08-20T06-43-44-976Z.yml | 4 + .../page-2026-08-20T06-43-47-532Z.yml | 31 + ...order-pipeline-investigation-2026-08-20.md | 431 ++++++++++ repairs-list-mobile.jpg | Bin 0 -> 3493 bytes src/app/(app)/quotes/archive/page.tsx | 76 +- src/app/(app)/quotes/page.tsx | 52 +- src/app/(app)/repairs/[id]/page.tsx | 789 ++++++++++++++++++ src/app/(app)/repairs/page.tsx | 516 ++++++++++-- .../[recordId]/[filename]/route.ts | 7 + src/app/api/v1/orders/lookup/route.ts | 44 + src/app/api/v1/quotes/archive/route.ts | 46 +- src/app/api/v1/quotes/search/route.ts | 56 +- .../v1/repairs/[id]/files/[field]/route.ts | 103 +++ .../[id]/generate-delivery-note/route.ts | 33 + src/app/api/v1/repairs/[id]/route.ts | 83 ++ src/app/api/v1/repairs/form-data/route.ts | 32 + src/app/api/v1/repairs/route.ts | 94 +++ src/components/data-table.tsx | 28 +- src/components/ui/order-search.tsx | 261 ++++++ src/lib/server/quotes-search-service.test.ts | 128 ++- src/lib/server/quotes-search-service.ts | 117 ++- src/lib/server/quotes-service.test.ts | 31 +- src/lib/server/quotes-service.ts | 62 +- src/lib/server/repairs-service.ts | 642 ++++++++++++++ src/lib/types.ts | 40 + 32 files changed, 3865 insertions(+), 154 deletions(-) create mode 100644 .playwright-mcp/console-2026-08-20T06-41-57-440Z.log create mode 100644 .playwright-mcp/console-2026-08-20T06-42-55-620Z.log create mode 100644 .playwright-mcp/console-2026-08-20T06-43-42-418Z.log create mode 100644 .playwright-mcp/page-2026-08-20T06-42-01-523Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T06-42-34-924Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T06-42-58-756Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T06-43-00-617Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T06-43-44-976Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T06-43-47-532Z.yml create mode 100644 docs/audit/order-pipeline-investigation-2026-08-20.md create mode 100644 repairs-list-mobile.jpg create mode 100644 src/app/(app)/repairs/[id]/page.tsx create mode 100644 src/app/api/v1/orders/lookup/route.ts create mode 100644 src/app/api/v1/repairs/[id]/files/[field]/route.ts create mode 100644 src/app/api/v1/repairs/[id]/generate-delivery-note/route.ts create mode 100644 src/app/api/v1/repairs/[id]/route.ts create mode 100644 src/app/api/v1/repairs/form-data/route.ts create mode 100644 src/app/api/v1/repairs/route.ts create mode 100644 src/components/ui/order-search.tsx create mode 100644 src/lib/server/repairs-service.ts diff --git a/.playwright-mcp/console-2026-08-20T06-41-57-440Z.log b/.playwright-mcp/console-2026-08-20T06-41-57-440Z.log new file mode 100644 index 0000000..0ddf869 --- /dev/null +++ b/.playwright-mcp/console-2026-08-20T06-41-57-440Z.log @@ -0,0 +1,117 @@ +[ 817ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 3814ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 3953ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 3991ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4790ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 6867ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8316ms] [LOG] [Fast Refresh] done in 1449ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 11321ms] [WARNING] The resource http://localhost:4001/_next/static/media/4de1fea1a954a5b6-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11322ms] [WARNING] The resource http://localhost:4001/_next/static/media/eaead17c7dbfcd5d-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11322ms] [WARNING] The resource http://localhost:4001/_next/static/media/36966cca54120369-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11322ms] [WARNING] The resource http://localhost:4001/_next/static/media/7d4881bb7e1bf84d-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/ee40bb094c99a29a-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/13971731025ec697-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/7b0b24f36b1a6d0b-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/558ca1a6aa3cb55e-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/6d664cce900333ee-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/e4af272ccee01ff0-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 11324ms] [WARNING] The resource http://localhost:4001/_next/static/media/22a5144ee8d83bca-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14638ms] [WARNING] The resource http://localhost:4001/_next/static/media/4de1fea1a954a5b6-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/eaead17c7dbfcd5d-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/36966cca54120369-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/7d4881bb7e1bf84d-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/ee40bb094c99a29a-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/13971731025ec697-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/7b0b24f36b1a6d0b-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/558ca1a6aa3cb55e-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/6d664cce900333ee-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/e4af272ccee01ff0-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 14639ms] [WARNING] The resource http://localhost:4001/_next/static/media/22a5144ee8d83bca-s.p.woff2?v=1787208117573 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/repairs:0 +[ 15374ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 15620ms] Invalid or unexpected token +[ 18741ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 18859ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19756ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 19806ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 20460ms] [LOG] [Fast Refresh] done in 654ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 20487ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 20637ms] [LOG] [Fast Refresh] done in 150ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23050ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23269ms] [LOG] [Fast Refresh] done in 220ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24105ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24790ms] [LOG] [Fast Refresh] done in 685ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26188ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26491ms] [LOG] [Fast Refresh] done in 304ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27110ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27399ms] [LOG] [Fast Refresh] done in 289ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28052ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28273ms] [LOG] [Fast Refresh] done in 221ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28660ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28729ms] [LOG] [Fast Refresh] done in 68ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 29064ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 29237ms] [LOG] [Fast Refresh] done in 173ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 29967ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 30204ms] [LOG] [Fast Refresh] done in 237ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 30847ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 30861ms] Invalid or unexpected token +[ 32642ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32716ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 34450ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 34495ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 35528ms] [LOG] [Fast Refresh] done in 1034ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 35584ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 35870ms] [LOG] [Fast Refresh] done in 287ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36379ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36659ms] [LOG] [Fast Refresh] done in 279ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37191ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37304ms] [LOG] [Fast Refresh] done in 113ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37950ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 38254ms] [LOG] [Fast Refresh] done in 304ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 39304ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 39770ms] [LOG] [Fast Refresh] done in 467ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 40227ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 40456ms] [LOG] [Fast Refresh] done in 229ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 40886ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 41010ms] [LOG] [Fast Refresh] done in 125ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 41464ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 41581ms] [LOG] [Fast Refresh] done in 117ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 44512ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 48116ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 48190ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 48794ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 48811ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:4001/login:0 +[ 49410ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 49595ms] [LOG] [Fast Refresh] done in 181ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 49930ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50049ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50123ms] [LOG] [Fast Refresh] done in 74ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50244ms] [LOG] [Fast Refresh] done in 195ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50263ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50649ms] [LOG] [Fast Refresh] done in 387ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50704ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 50733ms] [LOG] [Fast Refresh] done in 28ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51356ms] [LOG] [Fast Refresh] done in 651ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51405ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51574ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51608ms] [LOG] [Fast Refresh] done in 35ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51806ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 51873ms] [LOG] [Fast Refresh] done in 67ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 52562ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 54272ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 54329ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 54477ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:4001/login:0 +[ 54557ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55044ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55353ms] [LOG] [Fast Refresh] done in 311ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55672ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55746ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55759ms] [LOG] [Fast Refresh] done in 14ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55802ms] [LOG] [Fast Refresh] done in 56ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 55813ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 56079ms] [LOG] [Fast Refresh] done in 267ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 56117ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 56147ms] [LOG] [Fast Refresh] done in 30ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 56826ms] [LOG] [Fast Refresh] done in 709ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 56932ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 57996ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 58068ms] [LOG] [Fast Refresh] done in 71ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 diff --git a/.playwright-mcp/console-2026-08-20T06-42-55-620Z.log b/.playwright-mcp/console-2026-08-20T06-42-55-620Z.log new file mode 100644 index 0000000..3cdd0e9 --- /dev/null +++ b/.playwright-mcp/console-2026-08-20T06-42-55-620Z.log @@ -0,0 +1,100 @@ +[ 497ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 586ms] Invalid or unexpected token +[ 4897ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4960ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 6811ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 6827ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8392ms] [LOG] [Fast Refresh] done in 1564ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8418ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 9182ms] [LOG] [Fast Refresh] done in 765ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 9296ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 9503ms] [LOG] [Fast Refresh] done in 208ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 9960ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 10213ms] [LOG] [Fast Refresh] done in 253ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 10405ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 10998ms] [LOG] [Fast Refresh] done in 594ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 11229ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 11748ms] [LOG] [Fast Refresh] done in 519ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 11939ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 12423ms] [LOG] [Fast Refresh] done in 485ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 12691ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 13247ms] [LOG] [Fast Refresh] done in 556ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 13812ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 14030ms] [LOG] [Fast Refresh] done in 217ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16510ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 16753ms] Invalid or unexpected token +[ 19528ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19596ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 20857ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 20916ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 22456ms] [LOG] [Fast Refresh] done in 1540ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 22512ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 22758ms] [LOG] [Fast Refresh] done in 245ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23061ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23477ms] [LOG] [Fast Refresh] done in 415ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23787ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23972ms] [LOG] [Fast Refresh] done in 186ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24143ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24184ms] [LOG] [Fast Refresh] done in 40ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24374ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24801ms] [LOG] [Fast Refresh] done in 426ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25027ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25622ms] [LOG] [Fast Refresh] done in 596ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25951ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26024ms] [LOG] [Fast Refresh] done in 72ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26667ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26728ms] [LOG] [Fast Refresh] done in 62ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27073ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27426ms] [LOG] [Fast Refresh] done in 353ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27716ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28658ms] [LOG] [Fast Refresh] done in 942ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 29180ms] Invalid or unexpected token +[ 29182ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 30906ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 31016ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 31677ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 31691ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32285ms] [LOG] [Fast Refresh] done in 595ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32301ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32811ms] [LOG] [Fast Refresh] done in 510ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32922ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 32978ms] [LOG] [Fast Refresh] done in 56ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 34288ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 34475ms] [LOG] [Fast Refresh] done in 188ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 34742ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 34798ms] [LOG] [Fast Refresh] done in 56ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 35821ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36031ms] [LOG] [Fast Refresh] done in 211ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36449ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36538ms] [LOG] [Fast Refresh] done in 90ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36722ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36755ms] [LOG] [Fast Refresh] done in 33ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36926ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 36994ms] [LOG] [Fast Refresh] done in 69ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37364ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37529ms] [LOG] [Fast Refresh] done in 165ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37815ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37879ms] [LOG] [Fast Refresh] done in 63ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 38560ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 40336ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 40509ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 40895ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 41401ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:4001/login:0 +[ 41732ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 41917ms] [LOG] [Fast Refresh] done in 186ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42164ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42235ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42252ms] [LOG] [Fast Refresh] done in 18ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42300ms] [LOG] [Fast Refresh] done in 65ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42310ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42558ms] [LOG] [Fast Refresh] done in 248ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42568ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 42703ms] [LOG] [Fast Refresh] done in 135ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 43390ms] [LOG] [Fast Refresh] done in 821ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 43436ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 43879ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 43971ms] [LOG] [Fast Refresh] done in 91ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 44208ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 44265ms] [LOG] [Fast Refresh] done in 57ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 44991ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 45710ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 diff --git a/.playwright-mcp/console-2026-08-20T06-43-42-418Z.log b/.playwright-mcp/console-2026-08-20T06-43-42-418Z.log new file mode 100644 index 0000000..555c6ce --- /dev/null +++ b/.playwright-mcp/console-2026-08-20T06-43-42-418Z.log @@ -0,0 +1,85 @@ +[ 1240ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 2340ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 2397ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 2753ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 3046ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:4001/login:0 +[ 3483ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 3692ms] [LOG] [Fast Refresh] done in 209ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 3961ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4036ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4059ms] [LOG] [Fast Refresh] done in 24ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4109ms] [LOG] [Fast Refresh] done in 73ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4122ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4577ms] [LOG] [Fast Refresh] done in 456ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4649ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 4744ms] [LOG] [Fast Refresh] done in 94ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 5799ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 5886ms] [LOG] [Fast Refresh] done in 87ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 6935ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 7151ms] [LOG] [Fast Refresh] done in 216ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 7355ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 7663ms] [LOG] [Fast Refresh] done in 307ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 7803ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8077ms] [LOG] [Fast Refresh] done in 273ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8244ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 8389ms] [LOG] [Fast Refresh] done in 144ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 10233ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 10387ms] Invalid or unexpected token +[ 14406ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 14518ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 14533ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 15309ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ :0 +[ 15325ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 15876ms] [LOG] [Fast Refresh] done in 551ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 15955ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16089ms] [LOG] [Fast Refresh] done in 134ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16313ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16373ms] [LOG] [Fast Refresh] done in 60ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16587ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16705ms] [LOG] [Fast Refresh] done in 118ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 16983ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 17039ms] [LOG] [Fast Refresh] done in 55ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 17403ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 17583ms] [LOG] [Fast Refresh] done in 180ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 18050ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 18150ms] [LOG] [Fast Refresh] done in 100ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19019ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19113ms] [LOG] [Fast Refresh] done in 93ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19397ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19471ms] [LOG] [Fast Refresh] done in 75ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19732ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 19849ms] [LOG] [Fast Refresh] done in 116ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 21168ms] [LOG] %c Console Ninja extension is connected to Next.js browser, background: rgb(30,30,30); color: rgb(255,213,92) see https://tinyurl.com/2vt8jxzw for more info. @ :0 +[ 23378ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23432ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23612ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "current-password"): (More info: https://goo.gl/9p2vKq) %o @ http://localhost:4001/login:0 +[ 23617ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 23808ms] [LOG] SW registered: /sw.js http://localhost:4001/ @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24437ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 24864ms] [LOG] [Fast Refresh] done in 427ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25201ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25312ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25353ms] [LOG] [Fast Refresh] done in 41ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25413ms] [LOG] [Fast Refresh] done in 100ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25426ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 25948ms] [LOG] [Fast Refresh] done in 523ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26013ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 26192ms] [LOG] [Fast Refresh] done in 179ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27177ms] [LOG] [Fast Refresh] done in 1164ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 27231ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28177ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28341ms] [LOG] [Fast Refresh] done in 164ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28588ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 28640ms] [LOG] [Fast Refresh] done in 53ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 +[ 37920ms] [WARNING] The resource http://localhost:4001/_next/static/media/6d664cce900333ee-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37920ms] [WARNING] The resource http://localhost:4001/_next/static/media/558ca1a6aa3cb55e-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37921ms] [WARNING] The resource http://localhost:4001/_next/static/media/e4af272ccee01ff0-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37921ms] [WARNING] The resource http://localhost:4001/_next/static/css/app/layout.css?v=1787208247695 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37921ms] [WARNING] The resource http://localhost:4001/_next/static/media/eaead17c7dbfcd5d-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/7b0b24f36b1a6d0b-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/13971731025ec697-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/22a5144ee8d83bca-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/36966cca54120369-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/ee40bb094c99a29a-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/7d4881bb7e1bf84d-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 +[ 37922ms] [WARNING] The resource http://localhost:4001/_next/static/media/4de1fea1a954a5b6-s.p.woff2?v=1787208242925 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:4001/login:0 diff --git a/.playwright-mcp/page-2026-08-20T06-42-01-523Z.yml b/.playwright-mcp/page-2026-08-20T06-42-01-523Z.yml new file mode 100644 index 0000000..293a778 --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-42-01-523Z.yml @@ -0,0 +1,3 @@ +- generic [active] [ref=e1]: + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=e9] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T06-42-34-924Z.yml b/.playwright-mcp/page-2026-08-20T06-42-34-924Z.yml new file mode 100644 index 0000000..d0f246f --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-42-34-924Z.yml @@ -0,0 +1,3 @@ +- generic [active] [ref=f2e1]: + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=f2e9] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T06-42-58-756Z.yml b/.playwright-mcp/page-2026-08-20T06-42-58-756Z.yml new file mode 100644 index 0000000..1aa0c1a --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-42-58-756Z.yml @@ -0,0 +1,2 @@ +- generic [active] [ref=f5e1]: + - region "Notifications alt+T" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T06-43-00-617Z.yml b/.playwright-mcp/page-2026-08-20T06-43-00-617Z.yml new file mode 100644 index 0000000..fc244bc --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-43-00-617Z.yml @@ -0,0 +1,3 @@ +- generic [active] [ref=f5e1]: + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=f5e9] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T06-43-44-976Z.yml b/.playwright-mcp/page-2026-08-20T06-43-44-976Z.yml new file mode 100644 index 0000000..046661d --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-43-44-976Z.yml @@ -0,0 +1,4 @@ +- generic [active] [ref=f10e1]: + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=f10e7] [cursor=pointer] + - alert [ref=f10e11] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T06-43-47-532Z.yml b/.playwright-mcp/page-2026-08-20T06-43-47-532Z.yml new file mode 100644 index 0000000..f2f2153 --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T06-43-47-532Z.yml @@ -0,0 +1,31 @@ +- generic [active] [ref=f10e1]: + - generic [ref=f10e13]: + - generic [ref=f10e15]: + - img "WFDASH" [ref=f10e17] + - generic [ref=f10e18]: WFDASH + - generic [ref=f10e19]: Business CRM + - generic [ref=f10e21]: + - tablist [ref=f10e22]: + - tab "Login" [selected] [ref=f10e23] + - tab "Register" [ref=f10e24] + - tab "Reset" [ref=f10e25] + - tabpanel "Login" [ref=f10e26]: + - generic [ref=f10e27]: + - generic [ref=f10e28]: + - generic [ref=f10e29]: Email + - textbox "Email" [ref=f10e30]: + - /placeholder: you@example.com + - text: david.els1@outlook.com + - generic [ref=f10e31]: + - generic [ref=f10e32]: Password + - textbox "Password" [ref=f10e33]: "1234567890" + - button "Sign In" [ref=f10e34] + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=f10e39] [cursor=pointer]: + - generic [ref=f10e42]: + - text: Rendering + - generic [ref=f10e43]: + - generic [ref=f10e44]: . + - generic [ref=f10e45]: . + - generic [ref=f10e46]: . + - alert [ref=f10e11] \ No newline at end of file diff --git a/docs/audit/order-pipeline-investigation-2026-08-20.md b/docs/audit/order-pipeline-investigation-2026-08-20.md new file mode 100644 index 0000000..73163e8 --- /dev/null +++ b/docs/audit/order-pipeline-investigation-2026-08-20.md @@ -0,0 +1,431 @@ +# Order Pipeline Investigation — 2026-08-20 + +Scope: investigate (1) hit-and-miss `contact_name` population on orders, +(2) duplicate-order creation when the same RFQ email arrives more than +once, and (3) general email-intake failures. Audit covers the n8n +workflows, the WFDASH Next.js app, and the live PocketBase data. + +## TL;DR + +| Symptom | Root cause | Severity | +|---|---|---| +| 84.6% of auto-created orders are duplicates of the same email | The IMAP poll keeps returning the same message every 5 minutes; the workflow has no `email_message_id` dedup check before `pb.collection('orders').create(...)` | **Critical** | +| `contact_name` is "hit and miss" (sometimes "Neville Benadé", sometimes "Nbenade\n", sometimes `\n`) | The contact name is sourced from a non-deterministic LLM call (no temperature cap, no fallback to deterministic email-signature parsing); the same email produces a different name on every poll cycle | **High** | +| The 9 most recent orders (2026-08-19) have empty `email_message_id` / `email_subject` | Those are counter-created orders — the post-create email field is not set on the manual `/counter` form. Not a bug, but it makes the "duplicate by msgid" dedup path useless for counter orders | **Low** | +| "Some emails fail" — orders created with `NO-REF`, `[NEEDS REVIEW]`, or `description = '[NEEDS REVIEW] AUTO-FLAGGED: ...'` | The AI Agent returns `order_items = []` whenever the PDF text extraction can't find a clean line-items table. The workflow then takes the "no items extracted" branch which writes a flag and a raw blob, but the *customer* / `contact_name` / `order_number` come from the same non-deterministic AI call so they may still be wrong | **Medium** | + +The dedup mechanism that already exists (`POST /api/v1/orders/duplicates/cancel`) is **reactive** — it finds duplicates after the fact and lets the operator cancel them. Nothing prevents the duplicates from being created in the first place. + +--- + +## 1. Pipeline overview + +### Active order-related n8n workflows (as of 2026-08-19 23:46 UTC) + +| ID | Name | Trigger | Purpose | +|---|---|---|---| +| `DWkxko0SS1uoelsg` | **WFDASH — ORDER MONITOR (Unified)** | Schedule (5 min) | The only pipeline that creates `orders` records. Polls IMAP, classifies, extracts via AI Agent, creates order + items. | +| `DWkxko0SS1uoelsg` (multi branch) | (same workflow) | (same) | The `(Multi)` branch handles multi-PO emails by splitting attachments by PO number detected in subject/body. | +| `O0yDcNVHEhA0Dd5n` | WFDASH — Multi-Order Fulfillment Pipeline | (other) | Creates `quotes` (not orders) for multi-PO emails via `/api/v1/quotes/from-email`. | +| `45h7IYegCmhtkV9t` | WFDASH — Multi-Order RFQ Fulfillment Pipeline | Schedule (2 min) | Creates `quotes` (not orders) for multi-PO emails via `/api/v1/quotes/from-email`. | +| `mXX2yu5svRurnfwO` | WFDASH - CATEGORISE ORDERS ITEMS | Schedule (1 min) | Backfills `ai_item_insights` and category enums. Does **not** create orders. | +| `paqlZICDbXbmY61V` | WFDASH — Populate Item Intel (Live) | Schedule | Item intel backfill. | +| `UqO3ayETx9cQHOzF` | WFDASH — Purchase Order Send | Webhook | Sends PO emails. | +| `GGS5zgVbdfsRyLcY` | ORDER-MARKUP-APPROVAL-LINKS | Webhook | WhatsApp/email markup-approval links. | +| `kVylcetxy858PcfP` | WFDASH — Error Handler | Error trigger | Telegram error alerts. | +| `aI0Ze2vwgp25ZgGH` | WFDASH — Error Notifier (Telegram) | Error trigger | Telegram error alerts. | +| `DDFN9NBmh95yG2Jk` | WFDASH — AI Insights Generator (v2) | Webhook | Re-runs AI insights on demand. | + +The ORDER MONITOR is the only thing that creates `orders` records from email. Its flow is: + +``` +Schedule Trigger (every 5 min) + -> CHECK FOR NEW MAIL (IMAP read, seen:false) + -> SetEmailFlags (mark as seen immediately) + -> Classify Email (Deterministic) (bounce / supplier / multi_order / customer / uncertain) + -> Route Email (Switch) (route to one of 5 branches) + -> Limit (limit to 1 — but parameters: {}, so probably a no-op) + -> Download ATTACHMENT + -> Split Attachments1 + -> Switch1 (by MIME type -> PDF / CSV / XLS / XLSX / TXT / image) + -> Extract PDF / CSV / XLS / XLSX / TXT (or Analyze image for image attachments) + -> Merge2 + -> Code in JavaScript4 (combine all extracted text) + -> AI Agent (LLM extraction -> JSON { order_info, order_items }) + -> If1 (if order_items.length > 0 -> single-order branch) + -> Search customers (PB getList by name/email fuzzy) + -> Create orders (PB create on orders collection) + -> Code in JavaScript2 (split order_items -> order_items rows) + -> Create order_items1 (PB create per item) + -> Aggregate + -> Merge1 + PocketBase1 (none) + MoveEmail email2 (move to INBOX.202608.Completed) +``` + +The single-order and multi-order branches converge at `Merge order completion` and +finally move the email to `INBOX.202608.Completed` (or `Failed` on the failure +branch). + +### PocketBase `orders` schema (live — `pbc_3527180448`) + +30 fields, **0 indexes**, **1 unique constraint** (the implicit primary key). +Relevant fields: + +- `order_number` (text, required) — no unique constraint +- `status` (select: `new` / `processing` / `processed` / `cancelled`) +- `company` (relation, required) +- `customer` (relation, optional) +- `contact_name` (text, optional) — written by the `Create orders` n8n node from the AI agent's `order_info.contact_name` +- `email_subject`, `email_body`, `email_sender`, `email_message_id` (text, optional) +- `rfq_number`, `description`, `notes`, `rep` (text, optional) + +The n8n `Create orders` node writes `email_message_id = $('Limit').item.json.headers['message-id']` — but the field is **read from the email header**, not from the actual Message-ID envelope of the IMAP fetch. If the IMAP proxy / email provider rewrites the header, this might come back empty or differ from what `emailMessageId` exposes elsewhere. + +--- + +## 2. The duplicate-order problem + +### What the data shows + +500 most-recent auto-created orders queried on 2026-08-19 23:46 UTC: + +- **423 / 500 (84.6%)** are duplicates — i.e. the same `email_message_id` appears on 2 or more `orders` records +- **50 / 127 distinct message_ids** are duplicated (the rest are singletons) +- The worst case: a single email `` produced **138 duplicate orders** — one every 5 minutes from 15:31 to 17:51 UTC on 2026-08-18, all with `order_number = ZA02PO26002142` +- Several emails have **9–10 duplicates created in the same second** (e.g. `MSP17933` at 19:10:27–19:10:28, `MSP17934` at 19:15:31). The same-second pattern means multiple workflow executions are processing the same email concurrently — the IMAP poll's `seen:false` filter is not preventing parallel runs from picking it up + +### Why it's happening + +Looking at the ORDER MONITOR (`DWkxko0SS1uoelsg`): + +1. **Schedule Trigger** fires every 5 minutes (`{"field":"minutes"}` with no + value, n8n defaults to 5min — confirmed by the 5-min spacing in execution + log `56337 (23:45)` -> `56320 (23:40)` -> `56301 (23:35)`). +2. **CHECK FOR NEW MAIL** (IMAP-enhanced node) — fetches every email with + `seen: false` from `INBOX`. Include parts `textContent, htmlContent, + markdownContent, attachmentsInfo, flags, size, bodyStructure, headers`. +3. **SetEmailFlags (mark on fetch)** — marks the fetched email as `Seen` + immediately. This is the ONLY thing preventing the same email from being + picked up again on the next 5-min tick. +4. **Limit** — parameters `{}`. Confirmed by inspecting the workflow JSON. + In n8n the Limit node with no `maxItems` defaults to 1, but the + "merge-2" + multi-attachment paths also feed back into it, so this limit + is unreliable across branches. +5. **Create orders** — the `n8n-nodes-pocketbase.pocketbaseHttp` node does + `pb.collection('orders').create({...})` with no pre-check for an + existing record with the same `email_message_id`. + +Three things have to be true for duplicates to happen, and they all are: + +- **A.** The IMAP poll keeps returning the same email. It happens because: + the `seen:false` flag is set in the SAME workflow execution that processes + the email — but if the workflow is retried (e.g. n8n re-triggers it after a + transient error), or if the user reloads the IMAP mailbox and the IMAP + server returns the same UID before the seen flag is committed, the next + poll picks it up again. Same-second duplicates (9 in `19:10:27–19:10:28`) + mean the workflow is running in parallel for the same email. +- **B.** There is no server-side constraint preventing it. The PocketBase + `orders` collection has **no unique index** on `email_message_id` and + no application-level pre-check in `createOrder` (`src/lib/server/orders-service.ts:1828`). + `pb.collection('orders').create({...})` will happily insert a duplicate. +- **C.** The 5-min polling cadence multiplies the damage: every 5 minutes + the same email gets reprocessed, and every cycle the AI extracts a + slightly different `contact_name` (see §3) and a slightly different + `order_number` if the AI misses the PO number on that particular call + (the duplicate `NO-REF` records are the AI failing to extract the PO). + +### Mitigations That Already Exist (Reactive Only) + +- **POST /api/v1/orders/duplicates** (`src/app/api/v1/orders/duplicates/route.ts`) + — `findDuplicateOrders()` groups by `order_number` (within 60s) and by a + line-aggregate heuristic. Returns the grouped list. The UI + (`src/components/duplicate-orders-dialog.tsx`) lets the operator bulk-cancel. +- **POST /api/v1/orders/{id}/cancel** — soft-cancels an order (all items + flip to `cancelled`, record stays in DB for audit). + +These are **post-creation** tools. They do not prevent the duplicates from +appearing in the first place. + +### Fixes (proposed) + +**A. Add a unique constraint on `email_message_id` in PB.** This is the +most robust fix — it makes the database reject duplicate inserts. Since the +field is currently `required: false`, the migration is: + +1. In the PB admin: index `CREATE UNIQUE INDEX idx_orders_email_message_id + ON orders (email_message_id) WHERE email_message_id != ''` (or a + partial unique index — Postgres/SQLite both support it). +2. Backfill: cancel the existing 423 duplicates (or use the existing + `cancelOrdersBulk` op) so the unique constraint can be created. +3. In the ORDER MONITOR workflow, **before** `Create orders`, add a + `Search orders (email_message_id = X)` step. If a record exists, + short-circuit and move the email to `INBOX.202608.Completed` with a + `replay_skipped` flag in the post-create log. This makes the workflow + idempotent even if the IMAP poll does fire on the same email. + +**B. In the app, harden `createOrder` to refuse duplicates.** With a +unique constraint in place, the second `create()` will throw a 409 — wrap +the `pb.collection('orders').create(...)` call in +`src/lib/server/orders-service.ts:1828` to catch the constraint violation +and treat it as a "duplicate email, skipped" success. The function should +return the existing record. + +**C. Tighten the IMAP poll.** Replace the `seen:false` filter with an +explicit `since` cursor: store the last-fetched UID in the workflow's +`staticData` and only fetch `UID > lastUID`. This works even if the seen +flag is reset by a reconnection. + +**D. Move the email ingestion off the 5-min poll.** The poll is the only +reason duplicates get 5-min apart. A webhook-based trigger (e.g. +Postmark inbound webhook -> Next.js `/api/v1/orders/intake`) would +eliminate the polling surface entirely. The CREATE QUOTE FROM EMAIL +endpoint (`POST /api/v1/quotes/from-email`) already accepts the same +payload, so the work is mostly plumbing. + +--- + +## 3. The `contact_name` hit-and-miss problem + +### What the data shows + +Same email `` (ZA02PO26002142 +from Nbenade@masterdrilling.com) produced these `contact_name` values across +its 138 duplicate orders: + +``` +Nbenade\n +Nbenade\n +N. Benade\n +Nicole Benade\n +Nbenade\n +Not specified\n +Nbenade\n +Drilling Technical Services (PTY) Ltd\n +Nbenade\n +Nico Benade\n +N. Benade\n +Nbenade\n +Nbenade\n +``` + +Plus empty-string `""` records (`~8% of the same-email sample`). Some +records have literally `"\n"` as the contact name — the AI returned JSON +like `{"contact_name": "\n"}` and it was stored verbatim. + +The relevant emails are simple: + +``` +Good Day. + +Please see attached copy of the order, please deliver soonest possible date. + +Regards +``` + +There is no signature in the email body for the AI to extract from. The +PO attachment is what contains the actual name (Neville Benadé), and the +AI Agent prompt requires it to look at the attachments first — but it's +not deterministic about what it picks out. + +### Why it's "hit and miss" + +The contact_name pipeline: + +1. **AI Agent** (`DWkxko0SS1uoelsg` "AI Agent" node) — Anthropic LLM call + with `hasOutputParser: true`. The system prompt: + - "Extract the actual End-User or Requester of the goods (Look for + fields like 'Requester', 'Deliver To', 'Attention', or names in the + Notes). Do NOT use the 'Buyer' or 'Created By' name if a Requester is + listed." + - There is **no `temperature` override** — Claude's default is 1.0. + - The LLM is non-deterministic by design and the prompt does not + enforce a JSON retry on invalid output. +2. **Search customers** (PB getList) — builds a filter from the LLM's + `contact_name`: + ```js + const nameKeywords = name + .replace(/mr\.|mrs\.|ms\.|mr|mrs|ms/gi, '') + .split(/[\s,]+/) + .filter(w => w.length > 2); + ``` + - `replace(/mr\.|.../gi, '')` only strips the literal prefix + "Mr./Mrs./Ms." — it doesn't strip "Mr " (no dot) or "N." (no Mr.) + - `.filter(w => w.length > 2)` drops keywords of length ≤ 2. So + "N. Benade" -> ["Benade"], "Nbenade" -> ["Nbenade"], "Neville Benadé" + -> ["Neville", "Benadé"]. + - The `customer ~ 'X'` substring filter then matches the customer + collection. If the AI returned "Nbenade" and the customer is + "Neville Benadé", the substring match **succeeds** because "Nbenade" + is a substring of no one — but the search is fuzzy enough to return + multiple matches (the `elementsPerPage: 12` returns up to 12). +3. **Create orders** — `customer = $('Search customers').item.json?.id || + 'xmd68ps32k1x5fu'` — the first match wins. If the AI returned names + that don't match any customer, the fallback ID is the placeholder + `xmd68ps32k1x5fu` (which is "Unknown - needs review" in the customers + table). +4. **Page rendering** — `src/app/(app)/orders/page.tsx:942`: + ```tsx + {order.contact_name && ( + {order.contact_name} + )} + ``` + Empty string `""` is falsy -> row shows no name. Newline `"\n"` is + truthy -> row shows just a separator dot with nothing after it. The + operator sees "GARY-JV-MR · Master Drilling · ·" with a dangling + separator and no name — clearly "miss". + +### Fixes (proposed) + +**A. Stop using the LLM for the contact name.** The email body has a +signature; the PDF has a "Requester:" line. Both are deterministic. Add a +pre-LLM regex pass: + +1. Parse the email body for the signature (everything after the last + `Regards` / `Kind regards` / `Best` / `Thanks` block, or the last 5 + non-empty lines). +2. Parse the PDF text for `Requester:`, `Deliver To:`, `Attention:`, + `End-User:`. The first one is the contact name. +3. Only call the LLM if both fail. + +If a deterministic parse succeeds, the LLM is skipped entirely — the +hit/miss disappears. + +**B. If you keep the LLM, constrain it.** Add to the system prompt: +- "If you cannot find a Requester/Deliver-To/Attention name, return the + empty string — do NOT return placeholder text like 'Unknown', 'Not + specified', or the company name." +- Emit `contact_name = ""` (not `"\n"`) when uncertain. The page's + truthy-check will hide it cleanly. +- Set `temperature: 0` on the Anthropic node. The LLM is still + non-deterministic for the same prompt + temperature, but the variance + drops substantially. + +**C. Post-process the LLM output.** Wrap the AI Agent call in a Code +node that: +- trims trailing newlines (`s.replace(/\s+$/,'')`) +- validates the result against the customers collection (search by + email first, then by name) — if a `customer` row exists with the same + `email`, use `customer.customer` as the contact name regardless of what + the LLM said. + +**D. Move the display sanitisation into the renderer.** Even if the LLM +keeps hallucinating newlines and trailing whitespace, the row should not +show a dangling separator. Patch `order.contact_name` to a JSX-safe +display: + +```tsx +const displayContact = (order.contact_name || "").trim(); +{displayContact && · {displayContact}} +``` + +This is a one-line fix and removes the visible "miss" symptom even if +the data is still messy. + +--- + +## 4. The "some emails fail" question + +Looking at the live data, two failure modes are visible: + +### 4a. AI returns empty `contact_name` / `order_items` + +The `If1` / `If1 (Multi)` branches check `$json.output.order_items.length` +and route: + +- `<= 0` -> "no items extracted" branch -> `Create order (no items extracted - raw)` / `Create order (single - no items - raw)` / `Create order (no PO detected - raw)`. The resulting order has: + - `description = "[NEEDS REVIEW] AUTO-FLAGGED: AI Agent could not extract any line items for this PO. Raw extracted text: ..."` + - the raw extracted text dumped in the description + - the email still moved to `INBOX.202608.Completed` + +This is the right fallback — the operator sees the flag. But the customer +search may still be wrong because the LLM returned garbage for the contact +name (see §3). The order lands on the board with a flag, but the +customer/contact fields may be incorrect. + +### 4b. AI returns "Unknown" / "Not specified" / company name as contact + +Same root cause as §3. The LLM hallucinates an answer instead of returning +empty. The Search customers query then searches for "Unknown" or "Not +specified" or "Drilling Technical Services" — and finds the company +record instead of the customer record. The order is created with the +**company** as the customer, which is the wrong relation. + +### 4c. The IMAP poll loses emails + +When the IMAP mailbox is polled, the `seen:false` filter is set in the +SAME workflow execution as the processing. If the workflow crashes +(retry, timeout, network error, PocketBase 500), the email is still +marked as seen but never processed. The next 5-min poll won't pick it up. +This is silent failure — the email is in limbo until the operator manually +checks the inbox. + +There is no `onError` feedback to the ORDER MONITOR. The "Error Handler" +n8n workflow (`kVylcetxy858PcfP`) only catches errors from workflows that +have the error workflow set. ORDER MONITOR does not. + +### Fixes (proposed) + +**A. Make the email flag roll back on error.** Wrap the `Create orders` ++ `Create order_items1` chain in a Rollback node: if either fails, set +`Seen = false` on the email so the next poll picks it up. + +**B. Mark the workflow as error-to-Workflow.** In n8n workflow Settings, +set `errorWorkflow: kVylcetxy858PcfP` so the Telegram error notifier +fires on any failure. + +**C. Add a sender-domain blacklist to the deterministic classifier.** The +classifier already has `supplierDomains = ['festo.com']` but that's +incomplete — most of the duplicates come from +`masterdrilling.com`, `mreee.co.za`, `harmony.co.za`. Add a `customer` +domain list (the inverse of supplier). The classifier already routes +"supplier" -> ignored. Mirror it with "customer" -> process, but make sure +the operator can override the list. + +**D. Add a fallback "create order from raw email" path.** If the AI +Agent fails to extract structured data after N retries, the workflow +should still create an order with the raw email body as the description +and a flag. This is already the "no items extracted" branch — but verify +the branch is reachable for ALL routes (not just the customer branch). + +--- + +## 5. Recommended fixes, ranked by impact / effort + +| # | Fix | Impact | Effort | +|---|---|---|---| +| 1 | **Add UNIQUE index on `orders.email_message_id`** in PB, backfill the 423 existing duplicates via the bulk-cancel tool, and pre-check in `createOrder` | Stops the duplicate flood at the DB level | 2 hours (admin UI + service-layer try/catch) | +| 2 | **Add a "Search orders by email_message_id" pre-check** in the ORDER MONITOR workflow before `Create orders` | Stops duplicates even if the IMAP poll is unreliable | 30 min in n8n | +| 3 | **Deterministic contact_name extraction** (regex signature + PO requester line) before the LLM call | Eliminates the "hit and miss" name pop | 1 day (depends on how many PO formats need handling) | +| 4 | **Constrain the LLM**: temperature 0, "return empty string when uncertain" rule | Reduces LLM hallucination | 15 min | +| 5 | **Renderer sanitisation**: trim `contact_name` and only render when non-empty | Hides the visible "miss" symptom with no data loss | 5 min | +| 6 | **Replace IMAP poll with a webhook intake** (Postmark inbound -> `/api/v1/orders/intake`) | Eliminates the 5-min polling surface entirely | 1 week (depends on the email provider) | +| 7 | **Wire the ORDER MONITOR to the error workflow** | Surfaces failures on Telegram instead of silently losing emails | 15 min | +| 8 | **Roll back Seen flag on workflow error** | Recovers emails that fail to process | 30 min in n8n | + +If we only do one thing: **#1 + #2** together. They stop the bleeding. If +we do two more: **#3 + #5** clean up the visible UX. #6 is the long-term +fix that fixes all polling-class bugs at once. + +--- + +## 6. Files / artefacts that were touched or referenced + +- n8n workflows (live, active): `DWkxko0SS1uoelsg`, `O0yDcNVHEhA0Dd5n`, `45h7IYegCmhtkV9t`, `mXX2yu5svRurnfwO`, `UqO3ayETx9cQHOzF`, `GGS5zgVbdfsRyLcY`, `kVylcetxy858PcfP`, `aI0Ze2vwgp25ZgGH`, `DDFN9NBmh95yG2Jk` +- App sources: `src/app/(app)/orders/page.tsx`, `src/components/duplicate-orders-dialog.tsx`, `src/app/api/v1/orders/duplicates/route.ts`, `src/app/api/v1/orders/create/route.ts`, `src/lib/server/orders-service.ts` (createOrder, findDuplicateOrders) +- PocketBase collections: `orders` (pbc_3527180448), `customers` (pbc_108570809), `companies` (pbc_3866053794) +- Docs: `docs/order-workflow-rules.md`, `docs/AGENT_INSTRUCTIONS.md` + +### Query commands used (reproducible) + +```bash +# List all orders, look at duplicates +curl -H "Authorization: Bearer $AGENT_API_KEY" \ + "https://wfdash.apphub.co.za/api/v1/orders?perPage=500&sort=-created&fields=id,order_number,contact_name,email_message_id,email_sender,created,email_subject" + +# Get a single n8n workflow's JSON +curl -H "X-N8N-API-KEY: $N8N_API_KEY" \ + "http://n8n-...sslip.io/api/v1/workflows/DWkxko0SS1uoelsg" + +# Get a single execution's data +curl -H "X-N8N-API-KEY: $N8N_API_KEY" \ + "http://n8n-...sslip.io/api/v1/executions/56337?includeData=true" +``` diff --git a/repairs-list-mobile.jpg b/repairs-list-mobile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..945d1b2ae680bbecb71c86d297c1d974e75b7839 GIT binary patch literal 3493 zcmeHIX;4#F6h1Et0kS|KfPf1wRTQ_-4r);tzzqeJC{;wwkeH;X0Sv@I6*6`~a7(e3 z0j$MRmm$$oML`(c#<)}xQ_CO%!B}Z5n39-Af+#V)Pp0A@?et$e!@2jp_r3F-_uX^9 zxiims&-nm6mn@832p9%{p#jc^*uB`8814$r@`bT+3kHdsfPyp=K%AB(;Vg;{T9dRk zi1J|Y3!F>gXJ+8fw*=C)-^JAdnx?&_=3QyGje<-*DtLmf2@3P@@~l4SAB0B1w5AM?!ak zPzdnn2>^N$fcMV;?#Zt>bbCv0L5LcO`btMb1Y1A=K@baRz=!Dwg~Ci=LzuH2Vt_~> z;1dN2O|mPQOd^rpDHIo1nmdg~b*ED43@=YQgUO&$J$*fy-Yg#uXKd|Pj@Q+c*ydfdwat}D_1tiNb!WZ)LJe(mnCxUi z=6FV#!z!#(e9-5Rn@#>7x9+dJsOuDe)$4?um{}>sjY@7#_@%Q)LN1<{dPti_PvXle zo9>)Dy?KgW6ff1}26{(qDp2TlZy$(LYnGN}87~Px{L!{UzO1Kp|2NY$0}BeqXSa;K zVyvgpZ`W9wuV|~|O^I>azHJ(lswh;Py`fegEfVazu9qBMb0F#K;%vx4Kd5(xzd?Do z^0y7jPc|Rb6jV_Hy&9eetF)1viM?{m==S;V_f=|zazDdX#xc{@$3Gor%wK!EqQDk5 zMWufEy*5kZgadveKV!gs!FN~nn?h3?FW;&Nsg;yBp>7AeH5G|OKs z4wA^Jq**lUt3rlA%TBGBQhPJg!MQWe5?EThM}Jkd&oVe>=EsmchXU("completed"); const [search, setSearch] = useState(""); const [searchInput, setSearchInput] = useState(""); + // Path A scoping — the archive search defaults to the recent + // window (90 days) so it's fast. The toggle below the search + // box lets the operator extend to the full archive when they + // really need to (e.g. a customer phoned about a long-closed + // order). + const [includeOlder, setIncludeOlder] = useState(false); const fetchArchive = useCallback(async () => { setLoading(true); @@ -77,6 +84,7 @@ export default function QuotesArchivePage() { view, }); if (search.trim()) params.set("search", search.trim()); + if (includeOlder) params.set("includeOlder", "1"); const resp = await fetch( `/api/v1/quotes/archive?${params.toString()}`, { headers: { Authorization: pbToken }, cache: "no-store" }, @@ -98,7 +106,7 @@ export default function QuotesArchivePage() { } finally { setLoading(false); } - }, [page, search, view]); + }, [page, search, view, includeOlder]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change @@ -138,7 +146,7 @@ export default function QuotesArchivePage() { Quotes Archive

- Historical quotes. The main quotes page shows + Historical quotes. The main quotes page shows only the active workflow tabs.

@@ -206,24 +214,41 @@ export default function QuotesArchivePage() { {/* Search */} -
- - setSearchInput(e.target.value)} - className="pl-9 pr-9" - /> - {searchInput && ( - - )} +
+
+ + setSearchInput(e.target.value)} + className="pl-9 pr-9" + /> + {searchInput && ( + + )} +
+
{loading ? ( @@ -265,7 +290,16 @@ export default function QuotesArchivePage() { {quotes.length} match{quotes.length === 1 ? "" : "es"} for{" "} “{search}” - + {" "} + {includeOlder ? ( + + (searched the full archive) + + ) : ( + + (searched the last 90 days) + + )} ) : ( <> diff --git a/src/app/(app)/quotes/page.tsx b/src/app/(app)/quotes/page.tsx index 3ef1814..cebe48b 100644 --- a/src/app/(app)/quotes/page.tsx +++ b/src/app/(app)/quotes/page.tsx @@ -154,6 +154,12 @@ export default function QuotesPage() { const [search, setSearch] = useState(""); const [searchInput, setSearchInput] = useState(""); const SEARCH_DEBOUNCE_MS = 250; + // Path A scoping — when true, the search route bypasses the + // 90-day default cutoff and searches the full history. The + // operator's common case is "where's the Acme quote I just + // did" (default = scoped), but the toggle lets them dig deeper + // when a customer phoned about a long-closed order, etc. + const [includeOlder, setIncludeOlder] = useState(false); // Aborts superseded fetches so an in-flight "A" request can't race // past a fresh "AC" request and clobber the UI with stale results. const fetchAbortRef = useRef(null); @@ -227,6 +233,13 @@ export default function QuotesPage() { q: search.trim(), perPage: "50", }); + // Path A scoping: the server defaults to the recent window + // (90 days). The page surfaces an "Include older" toggle for + // the rare case where the operator needs to search the full + // history. Without the flag the pushdown is bounded to the + // recent window so the search is fast even on the 8.5k-row + // collection. + if (includeOlder) params.set("includeOlder", "1"); // Search bypasses the active tab — matches the legacy // behaviour where every hit across every tab was returned // (the operator is hunting for a specific quote, not @@ -245,6 +258,8 @@ export default function QuotesPage() { }>; totalReturned: number; perTab: Record; + recencyLimited: boolean; + recencyCutoff: string | null; error?: string; }; if (!resp.ok) { @@ -341,7 +356,7 @@ export default function QuotesPage() { setLoading(false); } } - }, [page, search, activeTab]); + }, [page, search, activeTab, includeOlder]); // Debounce `searchInput` -> `search` so the fetch only fires // SEARCH_DEBOUNCE_MS after the operator stops typing. Without @@ -824,6 +839,41 @@ export default function QuotesPage() { error={error} searchPlaceholder="Search by number, customer, company, item, or price…" onSearch={(q) => { setSearchInput(q); }} + searchExtras={ + // Path A scoping — the search defaults to the last 90 days + // so it stays bounded and fast. The toggle lets the operator + // extend to the full history when they really need to + // (a customer phoned about a long-closed order, etc.). + // The label echoes the server's `recencyLimited` so the + // operator always knows which universe they're searching. +
+ {searchInput.trim() && ( + + {includeOlder + ? "Searching the full archive" + : "Searching the last 90 days"} + + )} + +
+ } page={page} totalPages={totalPages} onPageChange={setPage} diff --git a/src/app/(app)/repairs/[id]/page.tsx b/src/app/(app)/repairs/[id]/page.tsx new file mode 100644 index 0000000..e79b869 --- /dev/null +++ b/src/app/(app)/repairs/[id]/page.tsx @@ -0,0 +1,789 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { + ArrowLeft, + RefreshCw, + Truck, + CheckCircle2, + XCircle, + Save, + Upload, + Download, + Trash2, + Mail, + Image as ImageIcon, + StickyNote, + Pencil, +} from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { CustomerSearch } from "@/components/ui/customer-search"; +import { SupplierSearch } from "@/components/ui/supplier-search"; +import { OrderSearch } from "@/components/ui/order-search"; +import type { Customer, Supplier, Repair } from "@/lib/types"; + +const statusColors: Record< + string, + "default" | "secondary" | "destructive" | "outline" +> = { + new: "outline", + assessing: "secondary", + quoted: "secondary", + approved: "default", + in_progress: "secondary", + completed: "default", + cancelled: "destructive", +}; + +const statusLabels: Record = { + new: "New", + assessing: "Sent to supplier", + quoted: "Quote received", + approved: "Approved", + in_progress: "In progress", + completed: "Completed", + cancelled: "Cancelled", +}; + +const ALL_STATUSES = [ + "new", + "assessing", + "quoted", + "approved", + "in_progress", + "completed", + "cancelled", +] as const; + +type RepairDetail = Repair & { + expand?: { + customer?: Customer; + supplier?: Supplier; + company?: { id: string; company: string }; + order?: { id: string; order_number: string }; + }; +}; + +type FormDataResponse = { + customers: Customer[]; + suppliers: Supplier[]; +}; + +export default function RepairDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const router = useRouter(); + const [repairId, setRepairId] = useState(""); + const [repair, setRepair] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [generating, setGenerating] = useState(false); + const [error, setError] = useState(""); + const [lookups, setLookups] = useState({ + customers: [], + suppliers: [], + }); + + const [editing, setEditing] = useState(false); + const [edit, setEdit] = useState({ + customer: "", + company: "", + order: "", + supplier: "", + serial_no: "", + item_description: "", + fault_description: "", + notes: "", + status: "new" as (typeof ALL_STATUSES)[number], + }); + + const dnUploadRef = useRef(null); + const quoteUploadRef = useRef(null); + const poUploadRef = useRef(null); + + useEffect(() => { + params.then((p) => setRepairId(p.id)); + }, [params]); + + const fetchRepair = useCallback(async () => { + if (!repairId) return; + setLoading(true); + setError(""); + try { + const res = await fetch(`/api/v1/repairs/${repairId}`); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + const data: RepairDetail = await res.json(); + setRepair(data); + setEdit({ + customer: data.customer ?? "", + company: data.company ?? "", + order: data.order ?? "", + supplier: data.supplier ?? "", + serial_no: data.serial_no ?? "", + item_description: data.item_description ?? "", + fault_description: data.fault_description ?? "", + notes: data.notes ?? "", + status: (data.status ?? "new") as (typeof ALL_STATUSES)[number], + }); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load repair"); + } finally { + setLoading(false); + } + }, [repairId]); + + const fetchLookups = useCallback(async () => { + try { + const res = await fetch("/api/v1/repairs/form-data"); + if (!res.ok) return; + const data: FormDataResponse = await res.json(); + setLookups({ + customers: data.customers ?? [], + suppliers: data.suppliers ?? [], + }); + } catch { + // best-effort + } + }, []); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change + fetchRepair(); + // eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change + fetchLookups(); + }, [fetchRepair, fetchLookups]); + + const handleSave = async () => { + if (!repairId) return; + setSaving(true); + try { + const res = await fetch(`/api/v1/repairs/${repairId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + customer: edit.customer || null, + company: edit.company || null, + order: edit.order || null, + supplier: edit.supplier || null, + serial_no: edit.serial_no || null, + item_description: edit.item_description, + fault_description: edit.fault_description, + notes: edit.notes, + status: edit.status, + }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + toast.success("Repair updated"); + setEditing(false); + fetchRepair(); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Failed to update repair"); + } finally { + setSaving(false); + } + }; + + const handleSendToSupplier = async () => { + if (!repairId) return; + if (!repair?.supplier) { + toast.error("Set a supplier on this repair first."); + setEditing(true); + return; + } + setGenerating(true); + try { + const res = await fetch( + `/api/v1/repairs/${repairId}/generate-delivery-note`, + { method: "POST" }, + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + const data = await res.json(); + toast.success( + `Delivery note ${data.deliveryNoteNumber} generated — driver carries this to the supplier.`, + ); + fetchRepair(); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Failed to generate DN"); + } finally { + setGenerating(false); + } + }; + + const handleUploadFile = async ( + field: "delivery_note" | "quote" | "purchase_order", + file: File, + ) => { + if (!repairId) return; + const fd = new FormData(); + fd.append(field, file, file.name); + try { + const res = await fetch( + `/api/v1/repairs/${repairId}/files/${field}`, + { method: "POST", body: fd }, + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + const labels: Record = { + delivery_note: "Delivery note", + quote: "Supplier quote", + purchase_order: "Purchase order", + }; + toast.success(`${labels[field]} uploaded`); + fetchRepair(); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Upload failed"); + } + }; + + const handleDeleteFile = async ( + field: "delivery_note" | "quote" | "purchase_order", + filename: string, + ) => { + if (!repairId) return; + if (!confirm(`Delete ${filename}?`)) return; + try { + const res = await fetch( + `/api/v1/repairs/${repairId}/files/${field}?filename=${encodeURIComponent(filename)}`, + { method: "DELETE" }, + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + toast.success("File removed"); + fetchRepair(); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Delete failed"); + } + }; + + const handleDeleteRepair = async () => { + if (!repairId) return; + if (!confirm("Delete this repair record? This cannot be undone.")) return; + try { + const res = await fetch(`/api/v1/repairs/${repairId}`, { + method: "DELETE", + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? `HTTP ${res.status}`); + } + toast.success("Repair deleted"); + router.push("/repairs"); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Delete failed"); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error || !repair) { + return ( +
+ + + + {error || "Repair not found"} + + +
+ ); + } + + const customerEmail = repair.expand?.customer?.email ?? ""; + const deliveryNoteFiles = repair.delivery_note ?? []; + const quoteFiles = repair.quote ?? []; + const poFiles = repair.purchase_order ?? []; + const photoFiles = repair.photos ?? []; + + return ( +
+
+ +
+ + {/* Header */} +
+
+
+

+ {repair.repair_number} +

+ + {statusLabels[repair.status] || repair.status} + +
+

{repair.item_description}

+
+
+ {repair.status === "new" && ( + + )} + {(repair.status === "approved" || repair.status === "in_progress") && ( + + )} + {repair.status !== "completed" && repair.status !== "cancelled" && ( + + )} + +
+
+ +
+ {/* Main column */} +
+ {/* Job details */} + + + Job details + + + {editing ? ( +
+
+
+ + { + const c = lookups.customers.find((x) => x.id === id); + setEdit((p) => ({ + ...p, + customer: id, + company: c?.company ?? p.company, + })); + }} + customers={lookups.customers} + placeholder="Pick a customer…" + /> +
+
+ + setEdit((p) => ({ ...p, order: id }))} + placeholder="Search for an order…" + /> +
+
+
+ + setEdit((p) => ({ ...p, supplier: id }))} + suppliers={lookups.suppliers} + placeholder="Pick the supplier / repair shop…" + /> +
+
+ + + setEdit((p) => ({ ...p, serial_no: e.target.value })) + } + /> +
+
+ + + setEdit((p) => ({ ...p, item_description: e.target.value })) + } + /> +
+
+ +