diff --git a/.playwright-cli/console-2026-05-28T18-46-12-915Z.log b/.playwright-cli/console-2026-05-28T18-46-12-915Z.log new file mode 100644 index 00000000..32aa8169 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T18-46-12-915Z.log @@ -0,0 +1,26 @@ +[ 153ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 189ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 258154ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 258221ms] [LOG] [Fast Refresh] done in 30ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 278808ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 278917ms] [LOG] [Fast Refresh] done in 45ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 279028ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 279075ms] [LOG] [Fast Refresh] done in 146ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 292207ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 292253ms] [LOG] [Fast Refresh] done in 147ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 294385ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 294434ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 302250ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 302383ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 302496ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 302587ms] [LOG] [Fast Refresh] done in 193ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 316325ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 316364ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 334602ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 334670ms] [LOG] [Fast Refresh] done in 6ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 341048ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 341095ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 360207ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 360288ms] [LOG] [Fast Refresh] done in 26ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 360398ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 360463ms] [LOG] [Fast Refresh] done in 165ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T18-53-47-172Z.log b/.playwright-cli/console-2026-05-28T18-53-47-172Z.log new file mode 100644 index 00000000..fab5e142 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T18-53-47-172Z.log @@ -0,0 +1,444 @@ +[ 132ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 167ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 45113ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 45116ms] [LOG] [Fast Refresh] done in 104ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 413440ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 413549ms] [LOG] [Fast Refresh] done in 36ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 421253ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 421298ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 564453ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 564520ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 596067ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 596119ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 658504ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 658698ms] [LOG] [Fast Refresh] done in 22ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 699556ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 699701ms] [LOG] [Fast Refresh] done in 179ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 704652ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 704703ms] [LOG] [Fast Refresh] done in 25ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 711928ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 711985ms] [LOG] [Fast Refresh] done in 26ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 717449ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 717502ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 720410ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 720460ms] [LOG] [Fast Refresh] done in 22ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 729211ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 729258ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 741255ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 741303ms] [LOG] [Fast Refresh] done in 46ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 760947ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 760994ms] [LOG] [Fast Refresh] done in 63ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 811864ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 811902ms] [LOG] [Fast Refresh] done in 22ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 817279ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 817313ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 821355ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 821391ms] [LOG] [Fast Refresh] done in 36ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 840429ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 840492ms] [LOG] [Fast Refresh] done in 37ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 844732ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 844782ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1083520ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1083558ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1190805ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1190838ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1208774ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1208807ms] [LOG] [Fast Refresh] done in 26ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1252572ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1252590ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1341552ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1341803ms] [LOG] [Fast Refresh] done in 287ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1352744ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1352812ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1364239ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1364301ms] [LOG] [Fast Refresh] done in 88ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1368932ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1369016ms] [LOG] [Fast Refresh] done in 80ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1369166ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1369181ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1379088ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1379164ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1379315ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1379325ms] [LOG] [Fast Refresh] done in 111ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1386159ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1386233ms] [LOG] [Fast Refresh] done in 73ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1386409ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1386417ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1395707ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1395849ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1403503ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1403560ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1410513ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1410573ms] [LOG] [Fast Refresh] done in 17ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1559333ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1559401ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1572705ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1572763ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1582919ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1582977ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1598190ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1598268ms] [LOG] [Fast Refresh] done in 19ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1598372ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1598446ms] [LOG] [Fast Refresh] done in 175ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1617181ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1617248ms] [LOG] [Fast Refresh] done in 68ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1635397ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1635496ms] [LOG] [Fast Refresh] done in 125ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1635788ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1636124ms] [LOG] [Fast Refresh] done in 438ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1636225ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1636240ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1636341ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1636398ms] [LOG] [Fast Refresh] done in 158ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1639997ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1640061ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1733994ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1734013ms] [LOG] [Fast Refresh] done in 50ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1741949ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1741967ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1748519ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1748537ms] [LOG] [Fast Refresh] done in 37ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1763656ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1763700ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1764024ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1764180ms] [LOG] [Fast Refresh] done in 257ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1764281ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1764422ms] [LOG] [Fast Refresh] done in 242ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1772594ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1772644ms] [LOG] [Fast Refresh] done in 30ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1778990ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1779038ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1788310ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1788370ms] [LOG] [Fast Refresh] done in 51ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1798572ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1798629ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1809080ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1809136ms] [LOG] [Fast Refresh] done in 38ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1828232ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1828341ms] [LOG] [Fast Refresh] done in 82ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1828510ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1828864ms] [LOG] [Fast Refresh] done in 454ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1854652ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1854691ms] [LOG] [Fast Refresh] done in 53ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1866495ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1866539ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1874137ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1874194ms] [LOG] [Fast Refresh] done in 40ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1880976ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1881032ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1890648ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1890714ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1897931ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1897991ms] [LOG] [Fast Refresh] done in 17ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1903001ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1903059ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1907439ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1907502ms] [LOG] [Fast Refresh] done in 7ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1919791ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1919866ms] [LOG] [Fast Refresh] done in 31ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1919985ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1920037ms] [LOG] [Fast Refresh] done in 153ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1923658ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1923676ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1943186ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1943229ms] [LOG] [Fast Refresh] done in 37ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1948242ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1948272ms] [LOG] [Fast Refresh] done in 40ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1955589ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1955623ms] [LOG] [Fast Refresh] done in 35ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1987308ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1987364ms] [LOG] [Fast Refresh] done in 35ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1996862ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1996906ms] [LOG] [Fast Refresh] done in 44ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2087628ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2087667ms] [LOG] [Fast Refresh] done in 140ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2435236ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2435282ms] [LOG] [Fast Refresh] done in 45ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2435472ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2435511ms] [LOG] [Fast Refresh] done in 141ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2509396ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2509450ms] [LOG] [Fast Refresh] done in 50ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2509565ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2509625ms] [LOG] [Fast Refresh] done in 162ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2519221ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2519260ms] [LOG] [Fast Refresh] done in 141ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2544874ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2544957ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2555294ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2555303ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2566345ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2566354ms] [LOG] [Fast Refresh] done in 78ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2586281ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2586293ms] [LOG] [Fast Refresh] done in 55ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2603014ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2603024ms] [LOG] [Fast Refresh] done in 62ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2631945ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2631953ms] [LOG] [Fast Refresh] done in 79ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2808133ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2808136ms] [LOG] [Fast Refresh] done in 31ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2919028ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2919034ms] [LOG] [Fast Refresh] done in 107ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997397ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997434ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997703ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997707ms] [LOG] [Fast Refresh] done in 104ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997960ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2997996ms] [LOG] [Fast Refresh] done in 137ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3007244ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3007245ms] [LOG] [Fast Refresh] done in 101ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3056548ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3056628ms] [LOG] [Fast Refresh] done in 21ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3065644ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3065817ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072471ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072586ms] [LOG] [Fast Refresh] done in 140ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072701ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072732ms] [LOG] [Fast Refresh] done in 132ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072835ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3072912ms] [LOG] [Fast Refresh] done in 179ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3099110ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3099169ms] [LOG] [Fast Refresh] done in 69ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3133982ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3134054ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3134170ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3134222ms] [LOG] [Fast Refresh] done in 152ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3150214ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3150266ms] [LOG] [Fast Refresh] done in 24ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3408706ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3408838ms] [LOG] [Fast Refresh] done in 233ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3409234ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3409276ms] [LOG] [Fast Refresh] done in 143ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3409713ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3409798ms] [LOG] [Fast Refresh] done in 185ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3409958ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3410020ms] [LOG] [Fast Refresh] done in 164ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3657849ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3658582ms] [LOG] [Fast Refresh] done in 834ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3709348ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3709418ms] [LOG] [Fast Refresh] done in 43ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3779074ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3779234ms] [LOG] [Fast Refresh] done in 248ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3779866ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3779999ms] [LOG] [Fast Refresh] done in 234ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3780236ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3780285ms] [LOG] [Fast Refresh] done in 150ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3825667ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3825733ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3836720ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3836784ms] [LOG] [Fast Refresh] done in 16ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3848547ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3848825ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3851881ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3851937ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3886136ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3886178ms] [LOG] [Fast Refresh] done in 44ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3923301ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3923353ms] [LOG] [Fast Refresh] done in 59ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3929412ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3929451ms] [LOG] [Fast Refresh] done in 42ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3961302ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3961350ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3972419ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3972475ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4009333ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4009370ms] [LOG] [Fast Refresh] done in 39ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4026233ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4026277ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4031198ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4031234ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4040072ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4040108ms] [LOG] [Fast Refresh] done in 30ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4047328ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4047384ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4047614ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4047700ms] [LOG] [Fast Refresh] done in 186ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4054293ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4054370ms] [LOG] [Fast Refresh] done in 31ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4072484ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4072565ms] [LOG] [Fast Refresh] done in 114ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4072868ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4072900ms] [LOG] [Fast Refresh] done in 132ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4073884ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4073890ms] [LOG] [Fast Refresh] done in 107ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4074917ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4075364ms] [LOG] [Fast Refresh] done in 548ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4075464ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4075570ms] [LOG] [Fast Refresh] done in 207ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4078171ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4078239ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4108336ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4108439ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4108805ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4108974ms] [LOG] [Fast Refresh] done in 271ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4109447ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4109470ms] [LOG] [Fast Refresh] done in 123ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4110021ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4110072ms] [LOG] [Fast Refresh] done in 152ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4110174ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4110319ms] [LOG] [Fast Refresh] done in 246ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4117821ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4118052ms] [LOG] [Fast Refresh] done in 229ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4118436ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4118544ms] [LOG] [Fast Refresh] done in 208ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4119043ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4119061ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4119423ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4119433ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4120202ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4120220ms] [LOG] [Fast Refresh] done in 120ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4120418ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4120437ms] [LOG] [Fast Refresh] done in 120ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4120968ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4121013ms] [LOG] [Fast Refresh] done in 146ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4121227ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4121245ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4121485ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4121497ms] [LOG] [Fast Refresh] done in 113ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4123753ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4123763ms] [LOG] [Fast Refresh] done in 111ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4135088ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4135162ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4141046ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4141111ms] [LOG] [Fast Refresh] done in 54ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4520221ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4520223ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4710842ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4710956ms] [LOG] [Fast Refresh] done in 171ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4725065ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4725078ms] [LOG] [Fast Refresh] done in 43ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4730458ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4730600ms] [LOG] [Fast Refresh] done in 209ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4739611ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4739651ms] [LOG] [Fast Refresh] done in 39ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4769088ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4769121ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4786547ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4786566ms] [LOG] [Fast Refresh] done in 33ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4791475ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4791596ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4796145ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4796217ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4801886ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4801897ms] [LOG] [Fast Refresh] done in 49ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4806906ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4806973ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4824937ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4824949ms] [LOG] [Fast Refresh] done in 45ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4833659ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4833668ms] [LOG] [Fast Refresh] done in 63ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4838544ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4838603ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4888306ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4888366ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4897294ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4897353ms] [LOG] [Fast Refresh] done in 24ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918180ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918264ms] [LOG] [Fast Refresh] done in 87ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918431ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918519ms] [LOG] [Fast Refresh] done in 189ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918891ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4918943ms] [LOG] [Fast Refresh] done in 153ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4919211ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4919294ms] [LOG] [Fast Refresh] done in 184ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4919955ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4919979ms] [LOG] [Fast Refresh] done in 124ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920268ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920294ms] [LOG] [Fast Refresh] done in 126ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920482ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920652ms] [LOG] [Fast Refresh] done in 272ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920912ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4920958ms] [LOG] [Fast Refresh] done in 148ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4921219ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4921269ms] [LOG] [Fast Refresh] done in 149ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4921452ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4921497ms] [LOG] [Fast Refresh] done in 145ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4921778ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4922280ms] [LOG] [Fast Refresh] done in 603ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4922747ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4922810ms] [LOG] [Fast Refresh] done in 163ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4922995ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923051ms] [LOG] [Fast Refresh] done in 157ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923369ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923396ms] [LOG] [Fast Refresh] done in 128ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923555ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923639ms] [LOG] [Fast Refresh] done in 185ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4923992ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924081ms] [LOG] [Fast Refresh] done in 190ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924182ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924289ms] [LOG] [Fast Refresh] done in 208ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924595ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924627ms] [LOG] [Fast Refresh] done in 133ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4924886ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4925013ms] [LOG] [Fast Refresh] done in 227ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4925878ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4926520ms] [LOG] [Fast Refresh] done in 742ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4926722ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4926744ms] [LOG] [Fast Refresh] done in 123ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927153ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927211ms] [LOG] [Fast Refresh] done in 159ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927440ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927473ms] [LOG] [Fast Refresh] done in 132ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927577ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927634ms] [LOG] [Fast Refresh] done in 157ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927789ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4927819ms] [LOG] [Fast Refresh] done in 131ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928108ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928125ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928322ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928432ms] [LOG] [Fast Refresh] done in 211ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928721ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928801ms] [LOG] [Fast Refresh] done in 180ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928933ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4928942ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4929136ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4929151ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4929435ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4929645ms] [LOG] [Fast Refresh] done in 311ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4930336ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4931057ms] [LOG] [Fast Refresh] done in 821ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4932131ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4932189ms] [LOG] [Fast Refresh] done in 18ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4939777ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4939795ms] [LOG] [Fast Refresh] done in 46ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4947312ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4947329ms] [LOG] [Fast Refresh] done in 50ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4955260ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4955276ms] [LOG] [Fast Refresh] done in 46ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4962015ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4962051ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4969257ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4969396ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4992185ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4992341ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191032ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191120ms] [LOG] [Fast Refresh] done in 136ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191127ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191177ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191387ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5191675ms] [LOG] [Fast Refresh] done in 388ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5195466ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5195477ms] [LOG] [Fast Refresh] done in 46ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5199884ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5199897ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5212019ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5212031ms] [LOG] [Fast Refresh] done in 52ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5236330ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5236340ms] [LOG] [Fast Refresh] done in 61ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5246207ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5246215ms] [LOG] [Fast Refresh] done in 96ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5258720ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5258760ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5327591ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5327599ms] [LOG] [Fast Refresh] done in 81ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5341880ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5341893ms] [LOG] [Fast Refresh] done in 68ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5352134ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5352139ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5574549ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5574615ms] [LOG] [Fast Refresh] done in 64ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5583431ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5583439ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5813784ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5813791ms] [LOG] [Fast Refresh] done in 62ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5818336ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5818350ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T19-10-41-586Z.log b/.playwright-cli/console-2026-05-28T19-10-41-586Z.log new file mode 100644 index 00000000..d15abab8 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T19-10-41-586Z.log @@ -0,0 +1,120 @@ +[ 4595ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4630ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 69107ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 69144ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 77093ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 77101ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 176391ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 176424ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 194360ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 194393ms] [LOG] [Fast Refresh] done in 26ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 204816ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 204824ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 238159ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 238176ms] [LOG] [Fast Refresh] done in 33ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 247407ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 247413ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 327138ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 327389ms] [LOG] [Fast Refresh] done in 287ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 338330ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 338398ms] [LOG] [Fast Refresh] done in 114ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 349825ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 349887ms] [LOG] [Fast Refresh] done in 88ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 354518ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 354602ms] [LOG] [Fast Refresh] done in 80ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 354753ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 354767ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 364674ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 364750ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 364900ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 364911ms] [LOG] [Fast Refresh] done in 111ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 371745ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 371819ms] [LOG] [Fast Refresh] done in 73ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 371995ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 372003ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 381293ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 381435ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 389089ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 389146ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 396099ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 396159ms] [LOG] [Fast Refresh] done in 17ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 544919ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 544987ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 558291ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 558349ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 568505ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 568563ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 583776ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 583854ms] [LOG] [Fast Refresh] done in 19ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 583958ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 584032ms] [LOG] [Fast Refresh] done in 175ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 602767ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 602834ms] [LOG] [Fast Refresh] done in 68ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 620983ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621082ms] [LOG] [Fast Refresh] done in 125ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621374ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621710ms] [LOG] [Fast Refresh] done in 438ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621811ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621826ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621927ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 621984ms] [LOG] [Fast Refresh] done in 157ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 625583ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 625647ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 719581ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 719599ms] [LOG] [Fast Refresh] done in 51ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 727536ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 727553ms] [LOG] [Fast Refresh] done in 48ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 734106ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 734123ms] [LOG] [Fast Refresh] done in 38ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 749242ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 749286ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 749611ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 749766ms] [LOG] [Fast Refresh] done in 257ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 749867ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 750008ms] [LOG] [Fast Refresh] done in 242ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 758180ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 758230ms] [LOG] [Fast Refresh] done in 30ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 764576ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 764624ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 773896ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 773956ms] [LOG] [Fast Refresh] done in 51ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 784158ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 784215ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 794666ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 794722ms] [LOG] [Fast Refresh] done in 38ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 813818ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 813927ms] [LOG] [Fast Refresh] done in 82ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 814096ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 814450ms] [LOG] [Fast Refresh] done in 454ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 840238ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 840277ms] [LOG] [Fast Refresh] done in 53ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 852081ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 852125ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 859723ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 859780ms] [LOG] [Fast Refresh] done in 40ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 866562ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 866618ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 876234ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 876300ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 883517ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 883577ms] [LOG] [Fast Refresh] done in 17ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 888587ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 888645ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 893025ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 893088ms] [LOG] [Fast Refresh] done in 7ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 905378ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 905452ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 905571ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 905623ms] [LOG] [Fast Refresh] done in 153ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 909245ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 909262ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 928772ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 928815ms] [LOG] [Fast Refresh] done in 37ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 933828ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 933858ms] [LOG] [Fast Refresh] done in 40ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 941175ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 941209ms] [LOG] [Fast Refresh] done in 35ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 972894ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 972950ms] [LOG] [Fast Refresh] done in 35ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 982448ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 982492ms] [LOG] [Fast Refresh] done in 44ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T19-28-29-571Z.log b/.playwright-cli/console-2026-05-28T19-28-29-571Z.log new file mode 100644 index 00000000..9edf0e8f --- /dev/null +++ b/.playwright-cli/console-2026-05-28T19-28-29-571Z.log @@ -0,0 +1,363 @@ +[ 5114ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5126ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5244ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5268ms] [LOG] [Fast Refresh] done in 126ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 352837ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 352883ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 353073ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 353113ms] [LOG] [Fast Refresh] done in 141ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 426997ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 427051ms] [LOG] [Fast Refresh] done in 51ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 427166ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 427226ms] [LOG] [Fast Refresh] done in 162ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 436823ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 436861ms] [LOG] [Fast Refresh] done in 141ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 462475ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 462558ms] [LOG] [Fast Refresh] done in 15ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 472899ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 472924ms] [LOG] [Fast Refresh] done in 75ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 483949ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 483969ms] [LOG] [Fast Refresh] done in 80ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 503886ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 503906ms] [LOG] [Fast Refresh] done in 59ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 520619ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 520638ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 549547ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 549568ms] [LOG] [Fast Refresh] done in 81ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 725734ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 725737ms] [LOG] [Fast Refresh] done in 31ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 836628ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 836635ms] [LOG] [Fast Refresh] done in 107ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 914998ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 915035ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 915303ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 915308ms] [LOG] [Fast Refresh] done in 105ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 915559ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 915597ms] [LOG] [Fast Refresh] done in 137ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 924845ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 924846ms] [LOG] [Fast Refresh] done in 101ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 974149ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 974229ms] [LOG] [Fast Refresh] done in 21ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 983245ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 983418ms] [LOG] [Fast Refresh] done in 49ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990074ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990187ms] [LOG] [Fast Refresh] done in 140ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990302ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990333ms] [LOG] [Fast Refresh] done in 132ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990435ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 990513ms] [LOG] [Fast Refresh] done in 179ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1016711ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1016770ms] [LOG] [Fast Refresh] done in 69ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1051583ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1051655ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1051772ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1051823ms] [LOG] [Fast Refresh] done in 152ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1067815ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1067867ms] [LOG] [Fast Refresh] done in 24ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1326307ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1326439ms] [LOG] [Fast Refresh] done in 233ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1326834ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1326877ms] [LOG] [Fast Refresh] done in 144ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1327314ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1327399ms] [LOG] [Fast Refresh] done in 185ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1327559ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1327621ms] [LOG] [Fast Refresh] done in 164ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1575450ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1576183ms] [LOG] [Fast Refresh] done in 834ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1626949ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1627020ms] [LOG] [Fast Refresh] done in 43ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1696675ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1696835ms] [LOG] [Fast Refresh] done in 248ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1697467ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1697600ms] [LOG] [Fast Refresh] done in 234ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1697838ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1697886ms] [LOG] [Fast Refresh] done in 150ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1743268ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1743335ms] [LOG] [Fast Refresh] done in 14ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1754321ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1754385ms] [LOG] [Fast Refresh] done in 16ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1766149ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1766426ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1769482ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1769538ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1803737ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1803779ms] [LOG] [Fast Refresh] done in 43ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1840902ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1840954ms] [LOG] [Fast Refresh] done in 59ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1847013ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1847052ms] [LOG] [Fast Refresh] done in 42ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1878903ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1878951ms] [LOG] [Fast Refresh] done in 27ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1890020ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1890076ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1926934ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1926971ms] [LOG] [Fast Refresh] done in 39ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1943834ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1943878ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1948799ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1948835ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1957673ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1957709ms] [LOG] [Fast Refresh] done in 30ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1964929ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1964985ms] [LOG] [Fast Refresh] done in 32ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1965215ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1965301ms] [LOG] [Fast Refresh] done in 186ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1971894ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1971971ms] [LOG] [Fast Refresh] done in 31ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1990085ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1990167ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1990469ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1990501ms] [LOG] [Fast Refresh] done in 132ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1991483ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1991491ms] [LOG] [Fast Refresh] done in 108ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1992517ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1992964ms] [LOG] [Fast Refresh] done in 548ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1993065ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1993171ms] [LOG] [Fast Refresh] done in 207ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1995772ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1995839ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2025937ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2026040ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2026404ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2026575ms] [LOG] [Fast Refresh] done in 272ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027048ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027071ms] [LOG] [Fast Refresh] done in 123ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027620ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027673ms] [LOG] [Fast Refresh] done in 153ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027774ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2027920ms] [LOG] [Fast Refresh] done in 246ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2035422ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2035653ms] [LOG] [Fast Refresh] done in 233ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2036037ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2036145ms] [LOG] [Fast Refresh] done in 208ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2036644ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2036662ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2037024ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2037034ms] [LOG] [Fast Refresh] done in 110ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2037803ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2037821ms] [LOG] [Fast Refresh] done in 120ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038019ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038038ms] [LOG] [Fast Refresh] done in 120ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038569ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038614ms] [LOG] [Fast Refresh] done in 146ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038828ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2038846ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2039086ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2039098ms] [LOG] [Fast Refresh] done in 113ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2041354ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2041364ms] [LOG] [Fast Refresh] done in 111ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2052689ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2052762ms] [LOG] [Fast Refresh] done in 13ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2058647ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2058712ms] [LOG] [Fast Refresh] done in 54ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2437822ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2437824ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2628443ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2628559ms] [LOG] [Fast Refresh] done in 174ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2642668ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2642679ms] [LOG] [Fast Refresh] done in 46ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2648059ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2648201ms] [LOG] [Fast Refresh] done in 209ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2657212ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2657252ms] [LOG] [Fast Refresh] done in 39ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2686689ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2686722ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2704149ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2704167ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2709076ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2709197ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2713746ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2713818ms] [LOG] [Fast Refresh] done in 29ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2719490ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2719498ms] [LOG] [Fast Refresh] done in 52ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2724507ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2724574ms] [LOG] [Fast Refresh] done in 9ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2742540ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2742550ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2751263ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2751269ms] [LOG] [Fast Refresh] done in 66ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2756145ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2756204ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2805907ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2805967ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2814895ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2814954ms] [LOG] [Fast Refresh] done in 24ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2835781ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2835865ms] [LOG] [Fast Refresh] done in 90ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836033ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836120ms] [LOG] [Fast Refresh] done in 190ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836492ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836544ms] [LOG] [Fast Refresh] done in 153ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836812ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2836896ms] [LOG] [Fast Refresh] done in 184ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2837556ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2837580ms] [LOG] [Fast Refresh] done in 124ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2837868ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2837894ms] [LOG] [Fast Refresh] done in 126ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838082ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838253ms] [LOG] [Fast Refresh] done in 273ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838512ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838559ms] [LOG] [Fast Refresh] done in 148ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838820ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2838870ms] [LOG] [Fast Refresh] done in 150ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2839052ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2839097ms] [LOG] [Fast Refresh] done in 145ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2839379ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2839890ms] [LOG] [Fast Refresh] done in 612ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840348ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840411ms] [LOG] [Fast Refresh] done in 163ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840596ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840651ms] [LOG] [Fast Refresh] done in 156ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840970ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2840997ms] [LOG] [Fast Refresh] done in 128ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841157ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841240ms] [LOG] [Fast Refresh] done in 185ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841593ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841683ms] [LOG] [Fast Refresh] done in 190ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841784ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2841890ms] [LOG] [Fast Refresh] done in 208ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2842197ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2842228ms] [LOG] [Fast Refresh] done in 133ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2842487ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2842614ms] [LOG] [Fast Refresh] done in 227ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2843479ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2844128ms] [LOG] [Fast Refresh] done in 747ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2844323ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2844345ms] [LOG] [Fast Refresh] done in 122ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2844754ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2844812ms] [LOG] [Fast Refresh] done in 158ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845041ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845074ms] [LOG] [Fast Refresh] done in 134ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845177ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845234ms] [LOG] [Fast Refresh] done in 156ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845391ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845420ms] [LOG] [Fast Refresh] done in 131ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845709ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845726ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2845923ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846032ms] [LOG] [Fast Refresh] done in 210ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846322ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846401ms] [LOG] [Fast Refresh] done in 180ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846534ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846543ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846737ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2846752ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2847034ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2847245ms] [LOG] [Fast Refresh] done in 311ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2847736ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2847736ms] [LOG] [Fast Refresh] done in 102ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2847937ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2848658ms] [LOG] [Fast Refresh] done in 821ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2849732ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2849790ms] [LOG] [Fast Refresh] done in 18ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2857379ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2857396ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2864916ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2864930ms] [LOG] [Fast Refresh] done in 52ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2872863ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2872877ms] [LOG] [Fast Refresh] done in 48ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2879616ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2879652ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2886858ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2886997ms] [LOG] [Fast Refresh] done in 10ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2909786ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2909942ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3108633ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3108721ms] [LOG] [Fast Refresh] done in 135ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3108728ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3108778ms] [LOG] [Fast Refresh] done in 8ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3108988ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3109276ms] [LOG] [Fast Refresh] done in 388ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3113069ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3113078ms] [LOG] [Fast Refresh] done in 48ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3117489ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3117498ms] [LOG] [Fast Refresh] done in 51ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3129622ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3129632ms] [LOG] [Fast Refresh] done in 54ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3153935ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3153943ms] [LOG] [Fast Refresh] done in 64ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3163814ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3163829ms] [LOG] [Fast Refresh] done in 103ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3176321ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3176361ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3245196ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3245206ms] [LOG] [Fast Refresh] done in 85ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3259485ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3259495ms] [LOG] [Fast Refresh] done in 71ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3269738ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3269747ms] [LOG] [Fast Refresh] done in 69ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3492150ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3492216ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3501037ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3501049ms] [LOG] [Fast Refresh] done in 70ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3731387ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3731395ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3735937ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 3735951ms] [LOG] [Fast Refresh] done in 115ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4335292ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4335521ms] [LOG] [Fast Refresh] done in 34ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4378605ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4378616ms] [LOG] [Fast Refresh] done in 61ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4382596ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4382605ms] [LOG] [Fast Refresh] done in 73ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4395312ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4395323ms] [LOG] [Fast Refresh] done in 60ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4416440ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4416450ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4437586ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4437594ms] [LOG] [Fast Refresh] done in 70ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4444806ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4444815ms] [LOG] [Fast Refresh] done in 78ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4450421ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4450430ms] [LOG] [Fast Refresh] done in 56ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4457819ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4457828ms] [LOG] [Fast Refresh] done in 63ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4466679ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4466687ms] [LOG] [Fast Refresh] done in 65ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4472142ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4472151ms] [LOG] [Fast Refresh] done in 54ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4484586ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4484715ms] [LOG] [Fast Refresh] done in 11ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4499632ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4499641ms] [LOG] [Fast Refresh] done in 76ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4506957ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4506962ms] [LOG] [Fast Refresh] done in 78ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4507062ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4507328ms] [LOG] [Fast Refresh] done in 366ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511048ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511053ms] [LOG] [Fast Refresh] done in 64ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511163ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511301ms] [LOG] [Fast Refresh] done in 240ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511403ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4511442ms] [LOG] [Fast Refresh] done in 140ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4522599ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4522644ms] [LOG] [Fast Refresh] done in 25ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4522750ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4522950ms] [LOG] [Fast Refresh] done in 300ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4528822ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4528834ms] [LOG] [Fast Refresh] done in 112ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4538503ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4538531ms] [LOG] [Fast Refresh] done in 45ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4538632ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4538825ms] [LOG] [Fast Refresh] done in 292ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4549456ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4549473ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4607219ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4607222ms] [LOG] [Fast Refresh] done in 104ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 5131549ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[22700020ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[27108661ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[27147203ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[34372259ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[36535862ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37485507ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37486513ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37487518ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37488525ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37766757ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37771768ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37776779ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37781787ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37786794ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37791801ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[37796804ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=9Sn5FM23e_Y0fnp6QZKuh' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 diff --git a/.playwright-cli/console-2026-05-28T20-27-42-245Z.log b/.playwright-cli/console-2026-05-28T20-27-42-245Z.log new file mode 100644 index 00000000..bda387e1 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-27-42-245Z.log @@ -0,0 +1,2 @@ +[ 1879ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1915ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-27-52-869Z.log b/.playwright-cli/console-2026-05-28T20-27-52-869Z.log new file mode 100644 index 00000000..82dc95c6 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-27-52-869Z.log @@ -0,0 +1,62 @@ +[ 615ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 648ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 48817ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 48825ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52391ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52399ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52420ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 52425ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 55265ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 55273ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 58512ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 58520ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 61143ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 61149ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 63663ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 63671ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 66642ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 66647ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 69449ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 69456ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 72452ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 72457ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 86928ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 86935ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 89422ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 89430ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 89449ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 89453ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 91876ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 91882ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 94071ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 94077ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 96301ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 96307ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 96327ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 96333ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 98392ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 98399ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 100553ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 100558ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 102697ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 102704ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 104782ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 104788ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 106865ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 106871ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 109234ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 109243ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 111548ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 111553ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 114094ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 114100ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 116636ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 116641ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 119029ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 119034ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 121368ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 121373ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 123643ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 123648ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 126025ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 126033ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-30-12-298Z.log b/.playwright-cli/console-2026-05-28T20-30-12-298Z.log new file mode 100644 index 00000000..5b36af93 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-30-12-298Z.log @@ -0,0 +1,2 @@ +[ 2248ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2257ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-30-18-499Z.log b/.playwright-cli/console-2026-05-28T20-30-18-499Z.log new file mode 100644 index 00000000..7c99c69c --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-30-18-499Z.log @@ -0,0 +1,13 @@ +[ 2250ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2257ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2281ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 2286ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 5616ms] [ERROR] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. Tiền mặt @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:3318 +[ 22465ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 22483ms] [LOG] [Fast Refresh] done in 70ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 27009ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 27023ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 39598ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 39603ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 47050ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 47057ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-31-10-905Z.log b/.playwright-cli/console-2026-05-28T20-31-10-905Z.log new file mode 100644 index 00000000..d2564257 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-31-10-905Z.log @@ -0,0 +1,6 @@ +[ 2673ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2682ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 12905ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 12913ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 16882ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 16888ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-31-29-083Z.log b/.playwright-cli/console-2026-05-28T20-31-29-083Z.log new file mode 100644 index 00000000..f0e227d3 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-31-29-083Z.log @@ -0,0 +1,18 @@ +[ 2479ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 2484ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4981ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 4988ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 9997ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 10004ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 14483ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 14489ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 18215ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 18222ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 22162ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 22170ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 26381ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 26388ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 30025ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 30033ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 49280ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 49290ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/console-2026-05-28T20-32-31-535Z.log b/.playwright-cli/console-2026-05-28T20-32-31-535Z.log new file mode 100644 index 00000000..7e598227 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-32-31-535Z.log @@ -0,0 +1,106 @@ +[ 103ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 139ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 28194ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 28198ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 41078ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 41087ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52149ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52157ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 63234ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 63240ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 73677ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 73683ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 84531ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 84539ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 96908ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 96914ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 493328ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 493557ms] [LOG] [Fast Refresh] done in 36ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 536648ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 536669ms] [LOG] [Fast Refresh] done in 68ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 540638ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 540655ms] ReferenceError: isPaidOrCompleted is not defined + at WorkflowScreen (http://localhost:3010/_next/static/chunks/apps_tpos-mvp-next_src_components_0v9yhs5._.js?id=%255Bproject%255D%252Fapps%252Ftpos-mvp-next%252Fsrc%252Fcomponents%252FTposPosExperience.tsx+%255Bapp-client%255D+%2528ecmascript%2529:2539:32) + at Object.react_stack_bottom_frame (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:15037:24) + at renderWithHooks (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:4620:24) + at updateFunctionComponent (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:6081:21) + at beginWork (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:6691:24) + at runWithFiberInDEV (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:965:74) + at performUnitOfWork (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:9555:97) + at workLoopSync (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:9449:40) + at renderRootSync (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:9433:13) + at performWorkOnRoot (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:9098:47) + at performSyncWorkOnRoot (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:10263:9) + at flushSyncWorkAcrossRoots_impl (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:10179:316) + at flushSyncWork$1 (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:9230:86) + at Object.scheduleRefresh (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-dom_08tl-yd._.js:299:13) + at http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_0ncn8g5._.js:391:33 + at Set.forEach () + at Object.performReactRefresh (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_0ncn8g5._.js:384:38) + at applyUpdate (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_0ncn8g5._.js:878:31) + at http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_0ncn8g5._.js:886:13 +[ 540660ms] [LOG] [Fast Refresh] done in 79ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 553358ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 553366ms] [WARNING] [Fast Refresh] performing full reload because your application had an unrecoverable error @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 553388ms] [LOG] [Fast Refresh] done in 71ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 558969ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 558980ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 574489ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 574512ms] [LOG] [Fast Refresh] done in 78ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 595630ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 595653ms] [LOG] [Fast Refresh] done in 78ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 602851ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 602873ms] [LOG] [Fast Refresh] done in 87ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 608465ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 608487ms] [LOG] [Fast Refresh] done in 64ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 615863ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 615882ms] [LOG] [Fast Refresh] done in 70ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 624722ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 624743ms] [LOG] [Fast Refresh] done in 72ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 630185ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 630204ms] [LOG] [Fast Refresh] done in 60ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 642622ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 642751ms] [LOG] [Fast Refresh] done in 12ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 657673ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 657696ms] [LOG] [Fast Refresh] done in 81ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 664993ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 664999ms] [LOG] [Fast Refresh] done in 80ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 665100ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 665364ms] [LOG] [Fast Refresh] done in 364ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669084ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669092ms] [LOG] [Fast Refresh] done in 67ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669198ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669337ms] [LOG] [Fast Refresh] done in 239ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669440ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 669478ms] [LOG] [Fast Refresh] done in 140ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 680635ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 680680ms] [LOG] [Fast Refresh] done in 28ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 680786ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 680986ms] [LOG] [Fast Refresh] done in 300ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 686858ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 686870ms] [LOG] [Fast Refresh] done in 112ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 696539ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 696567ms] [LOG] [Fast Refresh] done in 47ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 696669ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 696861ms] [LOG] [Fast Refresh] done in 292ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 707492ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 707509ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 765254ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 765258ms] [LOG] [Fast Refresh] done in 104ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 1289584ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[18858051ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[23266690ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[23305237ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[30530292ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[32693899ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33643545ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33644551ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33645556ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33646563ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33924795ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33929807ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33934817ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33939824ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33944830ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33949837ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33954844ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=SbhdanoDyeBMt0EvejOdQ' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 diff --git a/.playwright-cli/console-2026-05-28T20-44-31-373Z.log b/.playwright-cli/console-2026-05-28T20-44-31-373Z.log new file mode 100644 index 00000000..6fa79916 --- /dev/null +++ b/.playwright-cli/console-2026-05-28T20-44-31-373Z.log @@ -0,0 +1,114 @@ +[ 145ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 179ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 28413ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 28418ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 37459ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 37467ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 41486ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 41493ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 45300ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 45314ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 49103ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 49114ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52763ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 52771ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 67462ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 67471ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[ 80541ms] [ERROR] Failed to load resource: the server responded with a status of 400 (Bad Request) @ http://localhost:3010/api/bff/orders/2da66543-583f-45e4-b85e-cab8c3b50faa/cancel:0 +[ 569740ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[18138214ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[22546855ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[22585405ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[29810451ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[31974059ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[32923707ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[32924713ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[32925717ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[32926723ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33204955ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33209966ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33214975ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33219985ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33224991ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33229996ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[33235000ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=ydRxbhCggYpdYhC2kDB6r' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465544039ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3010/admin:0 +[465544085ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465544133ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465544221ms] error: could not create unique index "ux_staff_members_shop_employee_code" + at createCoreSchema (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?0:128:5) + at ensureDatabase (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?1:1059:5) + at query (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?2:1062:5) + at getSessionUser (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?3:3869:18) + at requirePortalRole (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?4:5043:18) + at AdminPage (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?5:5177:18) + at resolveErrorDev (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:1919:105) + at processFullStringRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2434:29) + at processFullBinaryRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2393:9) + at processBinaryChunk (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2502:221) + at progress (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2689:13) +[465544227ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3010/favicon.ico:0 +[465561673ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3010/admin:0 +[465561694ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465561701ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465561758ms] error: could not create unique index "ux_staff_members_shop_employee_code" + at createCoreSchema (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?0:128:5) + at ensureDatabase (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?1:1059:5) + at query (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?2:1062:5) + at getSessionUser (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?3:3869:18) + at requirePortalRole (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?4:5043:18) + at AdminPage (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?5:5177:18) + at resolveErrorDev (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:1919:105) + at processFullStringRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2434:29) + at processFullBinaryRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2393:9) + at processBinaryChunk (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2502:221) + at progress (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2689:13) +[465606845ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465607036ms] [LOG] [Fast Refresh] done in 217ms @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465607154ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3010/admin:0 +[465607171ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465607178ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465607232ms] error: could not create unique index "ux_staff_members_shop_employee_code" + at createCoreSchema (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?0:128:5) + at ensureDatabase (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?1:1059:5) + at query (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?2:1062:5) + at getSessionUser (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?3:3869:18) + at requirePortalRole (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?4:5043:18) + at AdminPage (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?5:5177:18) + at resolveErrorDev (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:1919:105) + at processFullStringRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2434:29) + at processFullBinaryRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2393:9) + at processBinaryChunk (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2502:221) + at progress (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2689:13) +[465635786ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3010/admin:0 +[465635801ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465635810ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465635868ms] error: could not create unique index "ux_staff_members_shop_employee_code" + at createCoreSchema (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?0:128:5) + at ensureDatabase (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?1:1059:5) + at query (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?2:1062:5) + at getSessionUser (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?3:3869:18) + at requirePortalRole (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?4:5043:18) + at AdminPage (about://React/Server/file:///Users/velikho/Desktop/WORKING/pos-system/microservices/apps/tpos-mvp-next/.next/dev/server/chunks/ssr/%5Broot-of-the-server%5D__0g_jbpa._.js?5:5177:18) + at resolveErrorDev (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:1919:105) + at processFullStringRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2434:29) + at processFullBinaryRow (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2393:9) + at processBinaryChunk (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2502:221) + at progress (http://localhost:3010/_next/static/chunks/0x~w_next_dist_compiled_react-server-dom-turbopack_0ctc-gz._.js:2689:13) +[465680084ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465681087ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465682089ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465683091ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465684092ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465685094ms] [ERROR] WebSocket connection to 'ws://localhost:3010/_next/webpack-hmr?id=VDJJvOVZf9uYmM7Bqp0gN' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_client_0vjm67r._.js:13322 +[465690102ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465690652ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465690659ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465698132ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465698140ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465711877ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465711885ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465782323ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465782329ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465790628ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 +[465790636ms] [LOG] [HMR] connected @ http://localhost:3010/_next/static/chunks/0x~w_next_dist_0c-ghxa._.js:2477 diff --git a/.playwright-cli/page-2026-05-28T18-45-39-661Z.yml b/.playwright-cli/page-2026-05-28T18-45-39-661Z.yml new file mode 100644 index 00000000..17ffd564 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T18-45-39-661Z.yml @@ -0,0 +1,65 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 01:45 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Dashboard" [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Dashboard + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e147]: + - generic [ref=e148]: + - generic [ref=e184]: + - generic [ref=e185]: Chi tiết hóa đơn + - strong [ref=e186]: "#2DA66543" + - button "Quay lại" [ref=e187] [cursor=pointer]: + - img [ref=e188] + - generic [ref=e190]: Quay lại + - generic [ref=e191]: + - generic [ref=e192]: + - generic [ref=e193]: HÓA ĐƠN + - strong [ref=e194]: "#2DA66543" + - generic [ref=e195]: + - generic [ref=e196]: Thời gian + - generic [ref=e197]: 01:28:33 29/05/2026 + - generic [ref=e198]: + - generic [ref=e199]: Trạng thái + - generic [ref=e200]: Đã thanh toán + - generic [ref=e201]: + - generic [ref=e202]: Thanh toán + - generic [ref=e203]: Tiền mặt + - generic [ref=e205]: + - generic [ref=e206]: Americano + - generic [ref=e207]: 1 x 45.000 ₫ + - generic [ref=e208]: 45.000 ₫ + - generic [ref=e209]: + - generic [ref=e210]: TỔNG CỘNG + - strong [ref=e211]: 45.000 ₫ + - button "In hóa đơn" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - generic [ref=e217]: In hóa đơn + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - alert [ref=e146] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T18-45-53-110Z.yml b/.playwright-cli/page-2026-05-28T18-45-53-110Z.yml new file mode 100644 index 00000000..2da9c21e --- /dev/null +++ b/.playwright-cli/page-2026-05-28T18-45-53-110Z.yml @@ -0,0 +1,82 @@ +- generic [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 01:45 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Dashboard" [active] [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Dashboard + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e218]: + - generic [ref=e219]: + - generic [ref=e220]: + - generic [ref=e221]: Dashboard bán hàng + - generic [ref=e222]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e223]: + - button "Hôm nay" [ref=e224] [cursor=pointer] + - button "7 ngày" [ref=e225] [cursor=pointer] + - button "30 ngày" [ref=e226] [cursor=pointer] + - generic [ref=e227]: + - generic [ref=e228]: + - generic [ref=e229]: Doanh thu + - strong [ref=e230]: 90.000 ₫ + - generic [ref=e231]: TB 45.000 ₫/đơn + - generic [ref=e232]: + - generic [ref=e233]: Đơn hàng + - strong [ref=e234]: "2" + - generic [ref=e235]: 29/05/2026 · Hôm nay + - generic [ref=e236]: + - generic [ref=e237]: Món bán ra + - strong [ref=e238]: "2" + - generic [ref=e239]: 1.0 món/đơn + - generic [ref=e240]: + - generic [ref=e241]: + - generic [ref=e242]: Món bán chạy + - generic [ref=e244]: + - generic [ref=e245]: Americano + - generic [ref=e246]: 2 đã bán + - generic [ref=e247]: 90.000 ₫ + - generic [ref=e249]: + - generic [ref=e250]: Hình thức thanh toán + - generic [ref=e253]: + - generic [ref=e254]: Tiền mặt + - strong [ref=e255]: 90.000 ₫ + - generic [ref=e258]: Doanh thu theo giờ + - generic [ref=e259]: + - generic [ref=e262]: 08h + - generic [ref=e265]: 09h + - generic [ref=e268]: 10h + - generic [ref=e271]: 11h + - generic [ref=e274]: 12h + - generic [ref=e277]: 13h + - generic [ref=e280]: 14h + - generic [ref=e283]: 15h + - generic [ref=e286]: 16h + - generic [ref=e289]: 17h + - generic [ref=e292]: 18h + - generic [ref=e295]: 19h + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - alert [ref=e146] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T18-46-13-169Z.yml b/.playwright-cli/page-2026-05-28T18-46-13-169Z.yml new file mode 100644 index 00000000..e4f767b8 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T18-46-13-169Z.yml @@ -0,0 +1,54 @@ +- generic [active] [ref=e1]: + - navigation [ref=e2]: + - link "aPOS" [ref=e3] [cursor=pointer]: + - /url: / + - generic [ref=e4]: + - link "Tính năng" [ref=e5] [cursor=pointer]: + - /url: /#features + - link "Bảng giá" [ref=e6] [cursor=pointer]: + - /url: /#pricing + - link "Đăng nhập" [ref=e7] [cursor=pointer]: + - /url: /auth/login + - link "Dùng thử miễn phí" [ref=e8] [cursor=pointer]: + - /url: /register + - main [ref=e9]: + - generic [ref=e10]: + - img [ref=e12] + - heading "aPOS Loyalty" [level=1] [ref=e14] + - paragraph [ref=e15]: Theo dõi điểm thưởng, voucher và lịch sử mua hàng từ QR menu đến POS. + - generic [ref=e16]: + - generic [ref=e17]: 50,000+ giao dịch/ngày + - generic [ref=e18]: 99.9% uptime + - generic [ref=e19]: + - generic [ref=e20]: + - generic [ref=e21]: + - img [ref=e22] + - text: CUSTOMER + - heading "Chào mừng bạn!" [level=2] [ref=e24] + - paragraph [ref=e25]: Đăng nhập để nhận ưu đãi và tích điểm thưởng + - generic [ref=e26]: + - img [ref=e27] + - generic [ref=e30]: Ưu đãi, tích điểm, lịch sử mua hàng + - generic [ref=e31]: + - generic [ref=e32]: Số điện thoại + - generic [ref=e33]: + - generic [ref=e34]: 🇻🇳 +84 + - textbox "Số điện thoại 🇻🇳 +84" [ref=e35]: + - /placeholder: 901 234 567 + - generic [ref=e36]: + - generic [ref=e37]: Hoặc đăng nhập bằng + - generic [ref=e38]: + - button "Zalo" [ref=e39] [cursor=pointer] + - button "Google" [ref=e40] [cursor=pointer] + - button "Facebook" [ref=e41] [cursor=pointer] + - button "Gửi mã OTP" [ref=e42] [cursor=pointer]: + - generic [ref=e43]: Gửi mã OTP + - img [ref=e44] + - generic [ref=e46]: + - link "Đăng ký ngay" [ref=e47] [cursor=pointer]: + - /url: /auth/register/customer + - link "Điều khoản" [ref=e48] [cursor=pointer]: + - /url: /about + - button "Open Next.js Dev Tools" [ref=e54] [cursor=pointer]: + - img [ref=e55] + - alert [ref=e58] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T18-53-47-407Z.yml b/.playwright-cli/page-2026-05-28T18-53-47-407Z.yml new file mode 100644 index 00000000..47956f44 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T18-53-47-407Z.yml @@ -0,0 +1,80 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - navigation [ref=e3]: + - generic [ref=e4]: + - link "aPOS" [ref=e5] [cursor=pointer]: + - /url: / + - generic [ref=e6]: + - link "Ngành hàng" [ref=e7] [cursor=pointer]: + - /url: /#features + - link "Giới thiệu" [ref=e8] [cursor=pointer]: + - /url: /about + - link "Dự án" [ref=e9] [cursor=pointer]: + - /url: /project + - link "Đăng nhập" [ref=e10] [cursor=pointer]: + - /url: /login + - link "Dùng thử miễn phí" [ref=e11] [cursor=pointer]: + - /url: /register + - generic [ref=e12]: + - generic [ref=e13]: + - img [ref=e14] + - generic [ref=e18]: TPOS + - generic [ref=e19]: Nền tảng POS đa ngành + - heading "Quản lý bán hàng cho từng mô hình vận hành" [level=1] [ref=e20] + - paragraph [ref=e21]: TPOS hỗ trợ Cafe, Nhà hàng, Karaoke, Spa và Bán lẻ với bán hàng tại quầy, quản lý ca, khách hàng thân thiết và báo cáo theo thời gian thực. + - generic [ref=e22]: + - link "Dùng thử miễn phí" [ref=e23] [cursor=pointer]: + - /url: /register + - img [ref=e24] + - text: Dùng thử miễn phí + - link "Đăng nhập" [ref=e26] [cursor=pointer]: + - /url: /auth/login + - img [ref=e27] + - text: Đăng nhập + - generic [ref=e30]: + - generic [ref=e31]: Dành cho điểm bán cần vận hành nhanh và rõ ràng + - generic [ref=e32]: + - generic [ref=e33]: POS tại quầy + - generic [ref=e34]: • + - generic [ref=e35]: Portal quản trị + - generic [ref=e36]: + - generic [ref=e37]: + - generic [ref=e38]: Chọn ngành hàng + - link "Vào portal" [ref=e39] [cursor=pointer]: + - /url: /auth/login + - text: Vào portal + - img [ref=e40] + - generic [ref=e42]: + - link "Cafe Order nhanh, pha chế, ca bán" [ref=e43] [cursor=pointer]: + - /url: /register + - img [ref=e44] + - generic [ref=e46]: Cafe + - paragraph [ref=e47]: Order nhanh, pha chế, ca bán + - link "Nhà hàng & F&B Bàn, bếp, thanh toán tách/gộp" [ref=e48] [cursor=pointer]: + - /url: /register + - img [ref=e49] + - generic [ref=e54]: Nhà hàng & F&B + - paragraph [ref=e55]: Bàn, bếp, thanh toán tách/gộp + - link "Karaoke Phòng, giờ hát, dịch vụ đi kèm" [ref=e56] [cursor=pointer]: + - /url: /register + - img [ref=e57] + - generic [ref=e60]: Karaoke + - paragraph [ref=e61]: Phòng, giờ hát, dịch vụ đi kèm + - link "TMV/Spa Lịch hẹn, liệu trình, khách hàng" [ref=e62] [cursor=pointer]: + - /url: /register + - img [ref=e63] + - generic [ref=e66]: TMV/Spa + - paragraph [ref=e67]: Lịch hẹn, liệu trình, khách hàng + - link "Bán lẻ Sản phẩm, tồn kho, khách thân thiết" [ref=e68] [cursor=pointer]: + - /url: /register + - img [ref=e69] + - generic [ref=e72]: Bán lẻ + - paragraph [ref=e73]: Sản phẩm, tồn kho, khách thân thiết + - generic [ref=e74]: + - generic [ref=e75]: + - img [ref=e76] + - generic [ref=e79]: Đã có tài khoản TPOS? + - link "Mở portal đăng nhập" [ref=e80] [cursor=pointer]: + - /url: /auth/login + - button "Open Next.js Dev Tools" [ref=e86] [cursor=pointer]: + - img [ref=e87] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-10-46-266Z.yml b/.playwright-cli/page-2026-05-28T19-10-46-266Z.yml new file mode 100644 index 00000000..5fd9fa1c --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-10-46-266Z.yml @@ -0,0 +1,109 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 02:10 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Dashboard" [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Dashboard + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e43]: + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: + - text: CAFE + - heading "Bán hàng" [level=1] [ref=e47] + - generic [ref=e48]: + - img [ref=e49] + - textbox [ref=e52]: + - /placeholder: SKU, barcode, tên món + - generic [ref=e53]: + - button "Tất cả" [ref=e54] [cursor=pointer] + - button "Coffee" [ref=e55] [cursor=pointer] + - button "Tea" [ref=e56] [cursor=pointer] + - button "Food" [ref=e57] [cursor=pointer] + - generic [ref=e58]: + - button "Americano 45.000 ₫ Tồn 38" [ref=e59] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Americano + - generic [ref=e64]: 45.000 ₫ + - generic [ref=e65]: Tồn 38 + - button "Latte 59.000 ₫ Tồn 35" [ref=e66] [cursor=pointer]: + - img [ref=e68] + - generic [ref=e70]: Latte + - generic [ref=e71]: 59.000 ₫ + - generic [ref=e72]: Tồn 35 + - button "Peach Tea 52.000 ₫ Tồn 28" [ref=e73] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e77]: Peach Tea + - generic [ref=e78]: 52.000 ₫ + - generic [ref=e79]: Tồn 28 + - button "Croissant 39.000 ₫ Tồn 16" [ref=e80] [cursor=pointer]: + - img [ref=e82] + - generic [ref=e84]: Croissant + - generic [ref=e85]: 39.000 ₫ + - generic [ref=e86]: Tồn 16 + - complementary [ref=e87]: + - generic [ref=e88]: + - generic [ref=e89]: Đơn hàng + - button [ref=e90] [cursor=pointer]: + - img [ref=e91] + - generic [ref=e95]: Chọn món từ thực đơn bên trái + - generic [ref=e96]: + - generic [ref=e97]: + - textbox "Mã voucher" [ref=e98] + - button "Áp dụng" [ref=e99] [cursor=pointer] + - generic [ref=e100]: + - button "Tiền mặt" [ref=e101] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e105]: Tiền mặt + - button "Thẻ" [disabled] [ref=e106]: + - img [ref=e107] + - generic [ref=e109]: Thẻ + - button "QR" [disabled] [ref=e110]: + - img [ref=e111] + - generic [ref=e113]: QR + - button "Chuyển khoản" [disabled] [ref=e114]: + - img [ref=e115] + - generic [ref=e119]: Chuyển khoản + - generic [ref=e120]: + - textbox "Khách đưa" [ref=e121] + - generic [ref=e122]: + - button "20.000 ₫" [ref=e123] [cursor=pointer] + - button "50.000 ₫" [ref=e124] [cursor=pointer] + - generic [ref=e125]: + - generic [ref=e126]: Tạm tính + - generic [ref=e127]: 0 ₫ + - generic [ref=e128]: Giảm giá + - generic [ref=e129]: 0 ₫ + - generic [ref=e130]: Tiền thối + - generic [ref=e131]: 0 ₫ + - strong [ref=e132]: Tổng cộng + - strong [ref=e133]: 0 ₫ + - button "Thanh toán" [disabled] [ref=e134]: + - img [ref=e135] + - text: Thanh toán + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-10-59-296Z.png b/.playwright-cli/page-2026-05-28T19-10-59-296Z.png new file mode 100644 index 00000000..060ca505 Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-10-59-296Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-11-05-786Z.png b/.playwright-cli/page-2026-05-28T19-11-05-786Z.png new file mode 100644 index 00000000..dffff7f0 Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-11-05-786Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-11-16-452Z.png b/.playwright-cli/page-2026-05-28T19-11-16-452Z.png new file mode 100644 index 00000000..dffff7f0 Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-11-16-452Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-11-58-744Z.yml b/.playwright-cli/page-2026-05-28T19-11-58-744Z.yml new file mode 100644 index 00000000..7c25b50a --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-11-58-744Z.yml @@ -0,0 +1,103 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - link [ref=e10] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e11] + - generic [ref=e15]: + - navigation [ref=e16]: + - button "Bán hàng" [ref=e17] [cursor=pointer]: + - img [ref=e18] + - generic [ref=e20]: Bán hàng + - button "Lịch sử" [ref=e21] [cursor=pointer]: + - img [ref=e22] + - generic [ref=e26]: Lịch sử + - button "Dashboard" [ref=e27] [cursor=pointer]: + - img [ref=e28] + - generic [ref=e30]: Dashboard + - button "Cài đặt" [ref=e31] [cursor=pointer]: + - img [ref=e32] + - generic [ref=e35]: Cài đặt + - generic [ref=e36]: + - generic [ref=e37]: + - generic [ref=e38]: + - generic [ref=e39]: + - text: CAFE + - heading "Bán hàng" [level=1] [ref=e40] + - generic [ref=e41]: + - img [ref=e42] + - textbox [ref=e45]: + - /placeholder: SKU, barcode, tên món + - generic [ref=e46]: + - button "Tất cả" [ref=e47] [cursor=pointer] + - button "Coffee" [ref=e48] [cursor=pointer] + - button "Tea" [ref=e49] [cursor=pointer] + - button "Food" [ref=e50] [cursor=pointer] + - generic [ref=e51]: + - button "Americano 45.000 ₫ Tồn 38" [ref=e52] [cursor=pointer]: + - img [ref=e54] + - generic [ref=e56]: Americano + - generic [ref=e57]: 45.000 ₫ + - generic [ref=e58]: Tồn 38 + - button "Latte 59.000 ₫ Tồn 35" [ref=e59] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Latte + - generic [ref=e64]: 59.000 ₫ + - generic [ref=e65]: Tồn 35 + - button "Peach Tea 52.000 ₫ Tồn 28" [ref=e66] [cursor=pointer]: + - img [ref=e68] + - generic [ref=e70]: Peach Tea + - generic [ref=e71]: 52.000 ₫ + - generic [ref=e72]: Tồn 28 + - button "Croissant 39.000 ₫ Tồn 16" [ref=e73] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e77]: Croissant + - generic [ref=e78]: 39.000 ₫ + - generic [ref=e79]: Tồn 16 + - complementary [ref=e80]: + - generic [ref=e81]: + - generic [ref=e82]: Đơn hàng + - button [ref=e83] [cursor=pointer]: + - img [ref=e84] + - generic [ref=e88]: Chọn món từ thực đơn bên trái + - generic [ref=e89]: + - generic [ref=e90]: + - textbox "Mã voucher" [ref=e91] + - button "Áp dụng" [ref=e92] [cursor=pointer] + - generic [ref=e93]: + - button "Tiền mặt" [ref=e94] [cursor=pointer]: + - img [ref=e95] + - generic [ref=e98]: Tiền mặt + - button "Thẻ" [disabled] [ref=e99]: + - img [ref=e100] + - generic [ref=e102]: Thẻ + - button "QR" [disabled] [ref=e103]: + - img [ref=e104] + - generic [ref=e106]: QR + - button "Chuyển khoản" [disabled] [ref=e107]: + - img [ref=e108] + - generic [ref=e112]: Chuyển khoản + - generic [ref=e113]: + - textbox "Khách đưa" [ref=e114] + - generic [ref=e115]: + - button "20.000 ₫" [ref=e116] [cursor=pointer] + - button "50.000 ₫" [ref=e117] [cursor=pointer] + - generic [ref=e118]: + - generic [ref=e119]: Tạm tính + - generic [ref=e120]: 0 ₫ + - generic [ref=e121]: Giảm giá + - generic [ref=e122]: 0 ₫ + - generic [ref=e123]: Tiền thối + - generic [ref=e124]: 0 ₫ + - strong [ref=e125]: Tổng cộng + - strong [ref=e126]: 0 ₫ + - button "Thanh toán" [disabled] [ref=e127]: + - img [ref=e128] + - text: Thanh toán + - button "Open Next.js Dev Tools" [ref=e135] [cursor=pointer]: + - img [ref=e136] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-12-24-613Z.png b/.playwright-cli/page-2026-05-28T19-12-24-613Z.png new file mode 100644 index 00000000..d13260bd Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-12-24-613Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-12-45-599Z.yml b/.playwright-cli/page-2026-05-28T19-12-45-599Z.yml new file mode 100644 index 00000000..b02c6da7 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-12-45-599Z.yml @@ -0,0 +1,70 @@ +- generic [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e140]: QA Cafe 282591 + - generic [ref=e9]: + - generic [ref=e141]: Online + - generic [ref=e143]: + - img [ref=e144] + - text: 02:12 + - link [ref=e10] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e11] + - generic [ref=e15]: + - navigation [ref=e16]: + - button "Bán hàng" [ref=e17] [cursor=pointer]: + - img [ref=e18] + - generic [ref=e20]: Bán hàng + - button "Lịch sử" [active] [ref=e21] [cursor=pointer]: + - img [ref=e22] + - generic [ref=e26]: Lịch sử + - button "Dashboard" [ref=e27] [cursor=pointer]: + - img [ref=e28] + - generic [ref=e30]: Dashboard + - button "Cài đặt" [ref=e31] [cursor=pointer]: + - img [ref=e32] + - generic [ref=e35]: Cài đặt + - generic [ref=e147]: + - generic [ref=e148]: + - generic [ref=e149]: + - img [ref=e150] + - textbox [ref=e153]: + - /placeholder: Tìm mã đơn, tên khách... + - generic "Bộ lọc lịch sử" [ref=e154]: + - button "Hôm nay" [ref=e155] [cursor=pointer] + - button "7 ngày" [ref=e156] [cursor=pointer] + - button "30 ngày" [ref=e157] [cursor=pointer] + - button "Tất cả" [ref=e158] [cursor=pointer] + - generic [ref=e159]: + - generic [ref=e160]: 2 đơn + - generic [ref=e161]: 2 đã thu + - strong [ref=e162]: 90.000 ₫ + - generic [ref=e163]: + - button "2DA66543 Đã thanh toán 1 món 45.000 ₫ 01:28 29-05 Tiền mặt" [ref=e164] [cursor=pointer]: + - generic [ref=e165]: + - generic [ref=e166]: 2DA66543 + - generic [ref=e167]: Đã thanh toán + - generic [ref=e168]: + - generic [ref=e169]: 1 món + - strong [ref=e170]: 45.000 ₫ + - generic [ref=e171]: + - generic [ref=e172]: 01:28 29-05 + - generic [ref=e173]: Tiền mặt + - button "BAABBC27 Đã thanh toán 1 món · A1 45.000 ₫ 01:28 29-05 Tiền mặt" [ref=e174] [cursor=pointer]: + - generic [ref=e175]: + - generic [ref=e176]: BAABBC27 + - generic [ref=e177]: Đã thanh toán + - generic [ref=e178]: + - generic [ref=e179]: 1 món · A1 + - strong [ref=e180]: 45.000 ₫ + - generic [ref=e181]: + - generic [ref=e182]: 01:28 29-05 + - generic [ref=e183]: Tiền mặt + - button "Open Next.js Dev Tools" [ref=e135] [cursor=pointer]: + - img [ref=e136] + - alert [ref=e139] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-12-52-093Z.png b/.playwright-cli/page-2026-05-28T19-12-52-093Z.png new file mode 100644 index 00000000..ab7633de Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-12-52-093Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-13-01-574Z.yml b/.playwright-cli/page-2026-05-28T19-13-01-574Z.yml new file mode 100644 index 00000000..3ead20c4 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-13-01-574Z.yml @@ -0,0 +1,82 @@ +- generic [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e140]: QA Cafe 282591 + - generic [ref=e9]: + - generic [ref=e141]: Online + - generic [ref=e143]: + - img [ref=e144] + - text: 02:13 + - link [ref=e10] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e11] + - generic [ref=e15]: + - navigation [ref=e16]: + - button "Bán hàng" [ref=e17] [cursor=pointer]: + - img [ref=e18] + - generic [ref=e20]: Bán hàng + - button "Lịch sử" [ref=e21] [cursor=pointer]: + - img [ref=e22] + - generic [ref=e26]: Lịch sử + - button "Dashboard" [active] [ref=e27] [cursor=pointer]: + - img [ref=e28] + - generic [ref=e30]: Dashboard + - button "Cài đặt" [ref=e31] [cursor=pointer]: + - img [ref=e32] + - generic [ref=e35]: Cài đặt + - generic [ref=e184]: + - generic [ref=e185]: + - generic [ref=e186]: + - generic [ref=e187]: Dashboard bán hàng + - generic [ref=e188]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e189]: + - button "Hôm nay" [ref=e190] [cursor=pointer] + - button "7 ngày" [ref=e191] [cursor=pointer] + - button "30 ngày" [ref=e192] [cursor=pointer] + - generic [ref=e193]: + - generic [ref=e194]: + - generic [ref=e195]: Doanh thu + - strong [ref=e196]: 90.000 ₫ + - generic [ref=e197]: TB 45.000 ₫/đơn + - generic [ref=e198]: + - generic [ref=e199]: Đơn hàng + - strong [ref=e200]: "2" + - generic [ref=e201]: 29/05/2026 · Hôm nay + - generic [ref=e202]: + - generic [ref=e203]: Món bán ra + - strong [ref=e204]: "2" + - generic [ref=e205]: 1.0 món/đơn + - generic [ref=e206]: + - generic [ref=e207]: + - generic [ref=e208]: Món bán chạy + - generic [ref=e210]: + - generic [ref=e211]: Americano + - generic [ref=e212]: 2 đã bán + - generic [ref=e213]: 90.000 ₫ + - generic [ref=e215]: + - generic [ref=e216]: Hình thức thanh toán + - generic [ref=e219]: + - generic [ref=e220]: Tiền mặt + - strong [ref=e221]: 90.000 ₫ + - generic [ref=e224]: Doanh thu theo giờ + - generic [ref=e225]: + - generic [ref=e228]: 08h + - generic [ref=e231]: 09h + - generic [ref=e234]: 10h + - generic [ref=e237]: 11h + - generic [ref=e240]: 12h + - generic [ref=e243]: 13h + - generic [ref=e246]: 14h + - generic [ref=e249]: 15h + - generic [ref=e252]: 16h + - generic [ref=e255]: 17h + - generic [ref=e258]: 18h + - generic [ref=e261]: 19h + - button "Open Next.js Dev Tools" [ref=e135] [cursor=pointer]: + - img [ref=e136] + - alert [ref=e139] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-13-07-682Z.png b/.playwright-cli/page-2026-05-28T19-13-07-682Z.png new file mode 100644 index 00000000..fc7846ba Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-13-07-682Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-14-06-464Z.yml b/.playwright-cli/page-2026-05-28T19-14-06-464Z.yml new file mode 100644 index 00000000..322c9f7f --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-14-06-464Z.yml @@ -0,0 +1,109 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 02:14 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Dashboard" [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Dashboard + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e43]: + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: + - text: CAFE + - heading "Bán hàng" [level=1] [ref=e47] + - generic [ref=e48]: + - img [ref=e49] + - textbox [ref=e52]: + - /placeholder: SKU, barcode, tên món + - generic [ref=e53]: + - button "Tất cả" [ref=e54] [cursor=pointer] + - button "Coffee" [ref=e55] [cursor=pointer] + - button "Tea" [ref=e56] [cursor=pointer] + - button "Food" [ref=e57] [cursor=pointer] + - generic [ref=e58]: + - button "Americano 45.000 ₫ Tồn 38" [ref=e59] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Americano + - generic [ref=e64]: 45.000 ₫ + - generic [ref=e65]: Tồn 38 + - button "Latte 59.000 ₫ Tồn 35" [ref=e66] [cursor=pointer]: + - img [ref=e68] + - generic [ref=e70]: Latte + - generic [ref=e71]: 59.000 ₫ + - generic [ref=e72]: Tồn 35 + - button "Peach Tea 52.000 ₫ Tồn 28" [ref=e73] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e77]: Peach Tea + - generic [ref=e78]: 52.000 ₫ + - generic [ref=e79]: Tồn 28 + - button "Croissant 39.000 ₫ Tồn 16" [ref=e80] [cursor=pointer]: + - img [ref=e82] + - generic [ref=e84]: Croissant + - generic [ref=e85]: 39.000 ₫ + - generic [ref=e86]: Tồn 16 + - complementary [ref=e87]: + - generic [ref=e88]: + - generic [ref=e89]: Đơn hàng + - button [ref=e90] [cursor=pointer]: + - img [ref=e91] + - generic [ref=e95]: Chọn món từ thực đơn bên trái + - generic [ref=e96]: + - generic [ref=e97]: + - textbox "Mã voucher" [ref=e98] + - button "Áp dụng" [ref=e99] [cursor=pointer] + - generic [ref=e100]: + - button "Tiền mặt" [ref=e101] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e105]: Tiền mặt + - button "Thẻ" [disabled] [ref=e106]: + - img [ref=e107] + - generic [ref=e109]: Thẻ + - button "QR" [disabled] [ref=e110]: + - img [ref=e111] + - generic [ref=e113]: QR + - button "Chuyển khoản" [disabled] [ref=e114]: + - img [ref=e115] + - generic [ref=e119]: Chuyển khoản + - generic [ref=e120]: + - textbox "Khách đưa" [ref=e121] + - generic [ref=e122]: + - button "20.000 ₫" [ref=e123] [cursor=pointer] + - button "50.000 ₫" [ref=e124] [cursor=pointer] + - generic [ref=e125]: + - generic [ref=e126]: Tạm tính + - generic [ref=e127]: 0 ₫ + - generic [ref=e128]: Giảm giá + - generic [ref=e129]: 0 ₫ + - generic [ref=e130]: Tiền thối + - generic [ref=e131]: 0 ₫ + - strong [ref=e132]: Tổng cộng + - strong [ref=e133]: 0 ₫ + - button "Thanh toán" [disabled] [ref=e134]: + - img [ref=e135] + - text: Thanh toán + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-14-21-926Z.yml b/.playwright-cli/page-2026-05-28T19-14-21-926Z.yml new file mode 100644 index 00000000..0baa73c9 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-14-21-926Z.yml @@ -0,0 +1,82 @@ +- generic [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 02:14 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Dashboard" [active] [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Dashboard + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e147]: + - generic [ref=e148]: + - generic [ref=e149]: + - generic [ref=e150]: Dashboard bán hàng + - generic [ref=e151]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e152]: + - button "Hôm nay" [ref=e153] [cursor=pointer] + - button "7 ngày" [ref=e154] [cursor=pointer] + - button "30 ngày" [ref=e155] [cursor=pointer] + - generic [ref=e156]: + - generic [ref=e157]: + - generic [ref=e158]: Doanh thu + - strong [ref=e159]: 90.000 ₫ + - generic [ref=e160]: TB 45.000 ₫/đơn + - generic [ref=e161]: + - generic [ref=e162]: Đơn hàng + - strong [ref=e163]: "2" + - generic [ref=e164]: 29/05/2026 · Hôm nay + - generic [ref=e165]: + - generic [ref=e166]: Món bán ra + - strong [ref=e167]: "2" + - generic [ref=e168]: 1.0 món/đơn + - generic [ref=e169]: + - generic [ref=e170]: + - generic [ref=e171]: Món bán chạy + - generic [ref=e173]: + - generic [ref=e174]: Americano + - generic [ref=e175]: 2 đã bán + - generic [ref=e176]: 90.000 ₫ + - generic [ref=e178]: + - generic [ref=e179]: Hình thức thanh toán + - generic [ref=e182]: + - generic [ref=e183]: Tiền mặt + - strong [ref=e184]: 90.000 ₫ + - generic [ref=e187]: Doanh thu theo giờ + - generic [ref=e188]: + - generic [ref=e191]: 08h + - generic [ref=e194]: 09h + - generic [ref=e197]: 10h + - generic [ref=e200]: 11h + - generic [ref=e203]: 12h + - generic [ref=e206]: 13h + - generic [ref=e209]: 14h + - generic [ref=e212]: 15h + - generic [ref=e215]: 16h + - generic [ref=e218]: 17h + - generic [ref=e221]: 18h + - generic [ref=e224]: 19h + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - alert [ref=e146] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-14-27-483Z.png b/.playwright-cli/page-2026-05-28T19-14-27-483Z.png new file mode 100644 index 00000000..c3d214cd Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-14-27-483Z.png differ diff --git a/.playwright-cli/page-2026-05-28T19-14-49-053Z.yml b/.playwright-cli/page-2026-05-28T19-14-49-053Z.yml new file mode 100644 index 00000000..f8da87b3 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-14-49-053Z.yml @@ -0,0 +1,109 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 02:14 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e22]: + - navigation [ref=e23]: + - button "Bán hàng" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - generic [ref=e27]: Bán hàng + - button "Lịch sử" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - generic [ref=e33]: Lịch sử + - button "Báo cáo" [ref=e34] [cursor=pointer]: + - img [ref=e35] + - generic [ref=e37]: Báo cáo + - button "Cài đặt" [ref=e38] [cursor=pointer]: + - img [ref=e39] + - generic [ref=e42]: Cài đặt + - generic [ref=e43]: + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: + - text: CAFE + - heading "Bán hàng" [level=1] [ref=e47] + - generic [ref=e48]: + - img [ref=e49] + - textbox [ref=e52]: + - /placeholder: SKU, barcode, tên món + - generic [ref=e53]: + - button "Tất cả" [ref=e54] [cursor=pointer] + - button "Coffee" [ref=e55] [cursor=pointer] + - button "Tea" [ref=e56] [cursor=pointer] + - button "Food" [ref=e57] [cursor=pointer] + - generic [ref=e58]: + - button "Americano 45.000 ₫ Tồn 38" [ref=e59] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Americano + - generic [ref=e64]: 45.000 ₫ + - generic [ref=e65]: Tồn 38 + - button "Latte 59.000 ₫ Tồn 35" [ref=e66] [cursor=pointer]: + - img [ref=e68] + - generic [ref=e70]: Latte + - generic [ref=e71]: 59.000 ₫ + - generic [ref=e72]: Tồn 35 + - button "Peach Tea 52.000 ₫ Tồn 28" [ref=e73] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e77]: Peach Tea + - generic [ref=e78]: 52.000 ₫ + - generic [ref=e79]: Tồn 28 + - button "Croissant 39.000 ₫ Tồn 16" [ref=e80] [cursor=pointer]: + - img [ref=e82] + - generic [ref=e84]: Croissant + - generic [ref=e85]: 39.000 ₫ + - generic [ref=e86]: Tồn 16 + - complementary [ref=e87]: + - generic [ref=e88]: + - generic [ref=e89]: Đơn hàng + - button [ref=e90] [cursor=pointer]: + - img [ref=e91] + - generic [ref=e95]: Chọn món từ thực đơn bên trái + - generic [ref=e96]: + - generic [ref=e97]: + - textbox "Mã voucher" [ref=e98] + - button "Áp dụng" [ref=e99] [cursor=pointer] + - generic [ref=e100]: + - button "Tiền mặt" [ref=e101] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e105]: Tiền mặt + - button "Thẻ" [disabled] [ref=e106]: + - img [ref=e107] + - generic [ref=e109]: Thẻ + - button "QR" [disabled] [ref=e110]: + - img [ref=e111] + - generic [ref=e113]: QR + - button "Chuyển khoản" [disabled] [ref=e114]: + - img [ref=e115] + - generic [ref=e119]: Chuyển khoản + - generic [ref=e120]: + - textbox "Khách đưa" [ref=e121] + - generic [ref=e122]: + - button "20.000 ₫" [ref=e123] [cursor=pointer] + - button "50.000 ₫" [ref=e124] [cursor=pointer] + - generic [ref=e125]: + - generic [ref=e126]: Tạm tính + - generic [ref=e127]: 0 ₫ + - generic [ref=e128]: Giảm giá + - generic [ref=e129]: 0 ₫ + - generic [ref=e130]: Tiền thối + - generic [ref=e131]: 0 ₫ + - strong [ref=e132]: Tổng cộng + - strong [ref=e133]: 0 ₫ + - button "Thanh toán" [disabled] [ref=e134]: + - img [ref=e135] + - text: Thanh toán + - button "Open Next.js Dev Tools" [ref=e142] [cursor=pointer]: + - img [ref=e143] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-28-34-740Z.yml b/.playwright-cli/page-2026-05-28T19-28-34-740Z.yml new file mode 100644 index 00000000..ec0ab50f --- /dev/null +++ b/.playwright-cli/page-2026-05-28T19-28-34-740Z.yml @@ -0,0 +1,198 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: Chọn phương thức + - generic [ref=e11]: Online + - generic [ref=e12]: + - complementary [ref=e13]: + - link "Barista queue" [ref=e14] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/barista-queue + - img [ref=e15] + - generic [ref=e17]: Barista queue + - link "Stamp card" [ref=e18] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/loyalty-stamp + - img [ref=e19] + - generic [ref=e23]: Stamp card + - link "Daily report" [ref=e24] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/daily-report + - img [ref=e25] + - generic [ref=e27]: Daily report + - link "Customer display" [ref=e28] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/queue-display + - img [ref=e29] + - generic [ref=e31]: Customer display + - link "Menu management" [ref=e32] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/menu-management + - img [ref=e33] + - generic [ref=e36]: Menu management + - link "Order customize" [ref=e37] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/order-customize + - img [ref=e38] + - generic [ref=e41]: Order customize + - link "Milk foam options" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/milk-foam-options + - img [ref=e43] + - generic [ref=e45]: Milk foam options + - link "Cafe journey" [ref=e46] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/cafe-journey + - img [ref=e47] + - generic [ref=e50]: Cafe journey + - link "Chọn phương thức" [ref=e51] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/method-select?vertical=cafe + - img [ref=e52] + - generic [ref=e54]: Chọn phương thức + - link "Thanh toán tiền mặt" [ref=e55] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/cash?vertical=cafe + - img [ref=e56] + - generic [ref=e58]: Thanh toán tiền mặt + - link "Thanh toán thẻ" [ref=e59] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/card?vertical=cafe + - img [ref=e60] + - generic [ref=e62]: Thanh toán thẻ + - link "Thanh toán QR" [ref=e63] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/qr?vertical=cafe + - img [ref=e64] + - generic [ref=e66]: Thanh toán QR + - link "Chuyển khoản" [ref=e67] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/bank-transfer?vertical=cafe + - img [ref=e68] + - generic [ref=e71]: Chuyển khoản + - link "Gift card" [ref=e72] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/gift-card?vertical=cafe + - img [ref=e73] + - generic [ref=e77]: Gift card + - link "Thanh toán một phần" [ref=e78] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/partial?vertical=cafe + - img [ref=e79] + - generic [ref=e81]: Thanh toán một phần + - link "Chờ thanh toán" [ref=e82] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/pending?vertical=cafe + - img [ref=e83] + - generic [ref=e86]: Chờ thanh toán + - link "Thanh toán thành công" [ref=e87] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/success?vertical=cafe + - img [ref=e88] + - generic [ref=e91]: Thanh toán thành công + - link "Sửa đơn" [ref=e92] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/order-edit?vertical=cafe + - img [ref=e93] + - generic [ref=e96]: Sửa đơn + - link "Giảm giá" [ref=e97] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/discount?vertical=cafe + - img [ref=e98] + - generic [ref=e101]: Giảm giá + - link "Chọn khách hàng" [ref=e102] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/customer?vertical=cafe + - img [ref=e103] + - generic [ref=e105]: Chọn khách hàng + - link "Chuyển bàn/phòng" [ref=e106] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/table-transfer?vertical=cafe + - img [ref=e107] + - generic [ref=e109]: Chuyển bàn/phòng + - link "Kiểm kho" [ref=e110] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/stock-out?vertical=cafe + - img [ref=e111] + - generic [ref=e114]: Kiểm kho + - link "Tra SKU/giá" [ref=e115] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/price-check?vertical=cafe + - img [ref=e116] + - generic [ref=e120]: Tra SKU/giá + - link "Cash drawer" [ref=e121] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/operations/cash-drawer?vertical=cafe + - img [ref=e122] + - generic [ref=e124]: Cash drawer + - link "Shift management" [ref=e125] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/operations/shift?vertical=cafe + - img [ref=e126] + - generic [ref=e130]: Shift management + - link "Pending orders" [ref=e131] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/operations/pending-orders?vertical=cafe + - img [ref=e132] + - generic [ref=e135]: Pending orders + - link "Quick sale" [ref=e136] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/operations/quick-sale?vertical=cafe + - img [ref=e137] + - generic [ref=e139]: Quick sale + - link "Split bill" [ref=e140] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/split-bill?vertical=cafe + - img [ref=e141] + - generic [ref=e143]: Split bill + - link "Void/refund" [ref=e144] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/dialog/void-refund?vertical=cafe + - img [ref=e145] + - generic [ref=e148]: Void/refund + - link "Tablet POS" [ref=e149] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/tablet + - img [ref=e150] + - generic [ref=e152]: Tablet POS + - link "Mobile POS" [ref=e153] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/mobile + - img [ref=e154] + - generic [ref=e156]: Mobile POS + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e159] + - generic [ref=e161]: + - generic [ref=e162]: CAFE WORKFLOW + - heading "Chọn phương thức" [level=1] [ref=e163] + - paragraph [ref=e164]: Chọn tiền mặt, thẻ, QR, chuyển khoản hoặc gift card. + - generic [ref=e165]: "Context ID: 2da66543-583f-45e4-b85e-cab8c3b50faa" + - generic [ref=e166]: + - article [ref=e167]: + - generic [ref=e168]: Catalog service + - strong [ref=e169]: "4" + - generic [ref=e170]: Sản phẩm + - article [ref=e171]: + - generic [ref=e172]: FnB service + - strong [ref=e173]: "4" + - generic [ref=e174]: Bàn/phòng + - article [ref=e175]: + - generic [ref=e176]: Order service + - strong [ref=e177]: "2" + - generic [ref=e178]: Đơn hàng + - article [ref=e179]: + - generic [ref=e180]: Payment ledger + - strong [ref=e181]: 90.000 ₫ + - generic [ref=e182]: Doanh thu + - generic [ref=e183]: + - generic [ref=e184]: + - text: PAYMENT CONTEXT + - heading "Đơn 2DA66543" [level=2] [ref=e185] + - paragraph [ref=e186]: 1 món · Paid · 45.000 ₫ + - link "Xem đơn hàng" [ref=e187] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/reports + - generic [ref=e188]: + - generic [ref=e189]: + - text: CHỌN PHƯƠNG THỨC + - heading "Thanh toán đơn 2DA66543" [level=2] [ref=e190] + - paragraph [ref=e191]: Tổng cần thu 45.000 ₫ + - generic [ref=e192]: + - link "Tiền mặt" [ref=e193] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/payment/cash?vertical=cafe&orderId=2da66543-583f-45e4-b85e-cab8c3b50faa + - img [ref=e194] + - generic [ref=e197]: Tiền mặt + - button "Thẻ" [disabled] [ref=e198]: + - img [ref=e199] + - generic [ref=e201]: Thẻ + - button "QR" [disabled] [ref=e202]: + - img [ref=e203] + - generic [ref=e205]: QR + - button "Chuyển khoản" [disabled] [ref=e206]: + - img [ref=e207] + - generic [ref=e211]: Chuyển khoản + - generic [ref=e212]: + - article [ref=e213]: + - strong [ref=e214]: 2DA66543 + - generic [ref=e215]: 1 món · Paid + - generic [ref=e216]: 45.000 ₫ + - article [ref=e217]: + - strong [ref=e218]: Bàn/phòng A1 + - generic [ref=e219]: 1 món · Paid + - generic [ref=e220]: 45.000 ₫ + - button "Open Next.js Dev Tools" [ref=e226] [cursor=pointer]: + - img [ref=e227] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T19-28-51-151Z.png b/.playwright-cli/page-2026-05-28T19-28-51-151Z.png new file mode 100644 index 00000000..7ed43a3e Binary files /dev/null and b/.playwright-cli/page-2026-05-28T19-28-51-151Z.png differ diff --git a/.playwright-cli/page-2026-05-28T20-27-44-227Z.yml b/.playwright-cli/page-2026-05-28T20-27-44-227Z.yml new file mode 100644 index 00000000..f07c3b38 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-27-44-227Z.yml @@ -0,0 +1,34 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - link "aPOS" [ref=e3] [cursor=pointer]: + - /url: / + - generic [ref=e4]: + - heading "Chọn loại tài khoản" [level=1] [ref=e5] + - paragraph [ref=e6]: Đăng nhập với vai trò phù hợp + - generic [ref=e7]: + - link "Chủ doanh nghiệp Quản lý toàn bộ hệ thống, cửa hàng, nhân viên" [ref=e8] [cursor=pointer]: + - /url: /auth/login/admin + - img [ref=e10] + - heading "Chủ doanh nghiệp" [level=2] [ref=e14] + - paragraph [ref=e15]: Quản lý toàn bộ hệ thống, cửa hàng, nhân viên + - link "Quản lý chi nhánh Quản lý chi nhánh, ca làm việc, báo cáo" [ref=e16] [cursor=pointer]: + - /url: /auth/login/branch + - img [ref=e18] + - heading "Quản lý chi nhánh" [level=2] [ref=e22] + - paragraph [ref=e23]: Quản lý chi nhánh, ca làm việc, báo cáo + - link "Nhân viên Thu ngân, barista, phục vụ, bếp" [ref=e24] [cursor=pointer]: + - /url: /auth/login/staff + - img [ref=e26] + - heading "Nhân viên" [level=2] [ref=e30] + - paragraph [ref=e31]: Thu ngân, barista, phục vụ, bếp + - link "Khách hàng Tích điểm, ưu đãi, lịch sử mua hàng" [ref=e32] [cursor=pointer]: + - /url: /auth/login/customer + - img [ref=e34] + - heading "Khách hàng" [level=2] [ref=e36] + - paragraph [ref=e37]: Tích điểm, ưu đãi, lịch sử mua hàng + - paragraph [ref=e38]: + - text: Chưa có tài khoản? + - link "Đăng ký ngay" [ref=e39] [cursor=pointer]: + - /url: /register + - button "Open Next.js Dev Tools" [ref=e45] [cursor=pointer]: + - img [ref=e46] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-27-53-576Z.yml b/.playwright-cli/page-2026-05-28T20-27-53-576Z.yml new file mode 100644 index 00000000..f07c3b38 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-27-53-576Z.yml @@ -0,0 +1,34 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - link "aPOS" [ref=e3] [cursor=pointer]: + - /url: / + - generic [ref=e4]: + - heading "Chọn loại tài khoản" [level=1] [ref=e5] + - paragraph [ref=e6]: Đăng nhập với vai trò phù hợp + - generic [ref=e7]: + - link "Chủ doanh nghiệp Quản lý toàn bộ hệ thống, cửa hàng, nhân viên" [ref=e8] [cursor=pointer]: + - /url: /auth/login/admin + - img [ref=e10] + - heading "Chủ doanh nghiệp" [level=2] [ref=e14] + - paragraph [ref=e15]: Quản lý toàn bộ hệ thống, cửa hàng, nhân viên + - link "Quản lý chi nhánh Quản lý chi nhánh, ca làm việc, báo cáo" [ref=e16] [cursor=pointer]: + - /url: /auth/login/branch + - img [ref=e18] + - heading "Quản lý chi nhánh" [level=2] [ref=e22] + - paragraph [ref=e23]: Quản lý chi nhánh, ca làm việc, báo cáo + - link "Nhân viên Thu ngân, barista, phục vụ, bếp" [ref=e24] [cursor=pointer]: + - /url: /auth/login/staff + - img [ref=e26] + - heading "Nhân viên" [level=2] [ref=e30] + - paragraph [ref=e31]: Thu ngân, barista, phục vụ, bếp + - link "Khách hàng Tích điểm, ưu đãi, lịch sử mua hàng" [ref=e32] [cursor=pointer]: + - /url: /auth/login/customer + - img [ref=e34] + - heading "Khách hàng" [level=2] [ref=e36] + - paragraph [ref=e37]: Tích điểm, ưu đãi, lịch sử mua hàng + - paragraph [ref=e38]: + - text: Chưa có tài khoản? + - link "Đăng ký ngay" [ref=e39] [cursor=pointer]: + - /url: /register + - button "Open Next.js Dev Tools" [ref=e45] [cursor=pointer]: + - img [ref=e46] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-27-55-658Z.yml b/.playwright-cli/page-2026-05-28T20-27-55-658Z.yml new file mode 100644 index 00000000..01e6c7ad --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-27-55-658Z.yml @@ -0,0 +1,45 @@ +- generic [active] [ref=e1]: + - button "Open Next.js Dev Tools" [ref=e45] [cursor=pointer]: + - img [ref=e46] + - alert [ref=e49] + - navigation [ref=e50]: + - link "aPOS" [ref=e51] [cursor=pointer]: + - /url: / + - generic [ref=e52]: + - link "Tính năng" [ref=e53] [cursor=pointer]: + - /url: /#features + - link "Bảng giá" [ref=e54] [cursor=pointer]: + - /url: /project#pricing + - link "Đăng nhập" [ref=e55] [cursor=pointer]: + - /url: /auth/login + - link "Dùng thử miễn phí" [ref=e56] [cursor=pointer]: + - /url: /register + - main [ref=e57]: + - generic [ref=e58]: + - generic [ref=e59]: + - generic [ref=e60]: + - img [ref=e61] + - text: QUẢN TRỊ + - heading "Đăng nhập Admin" [level=2] [ref=e64] + - paragraph [ref=e65]: Truy cập hệ thống quản trị aPOS + - generic [ref=e66]: + - img [ref=e67] + - generic [ref=e70]: Khu vực bảo mật cao + - generic [ref=e71]: + - generic [ref=e72]: Email quản trị viên + - generic [ref=e73]: + - img [ref=e74] + - textbox "Email quản trị viên" [ref=e77] + - generic [ref=e78]: + - generic [ref=e79]: Mật khẩu + - generic [ref=e80]: + - img [ref=e81] + - textbox "Mật khẩu" [ref=e84] + - button "Đăng nhập bảo mật" [ref=e85] [cursor=pointer]: + - generic [ref=e86]: Đăng nhập bảo mật + - img [ref=e87] + - generic [ref=e89]: + - link "Chi nhánh" [ref=e90] [cursor=pointer]: + - /url: /auth/login/branch + - link "Nhân viên" [ref=e91] [cursor=pointer]: + - /url: /auth/login/staff \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-28-11-971Z.yml b/.playwright-cli/page-2026-05-28T20-28-11-971Z.yml new file mode 100644 index 00000000..580f4845 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-28-11-971Z.yml @@ -0,0 +1,50 @@ +- generic [active] [ref=e1]: + - button "Open Next.js Dev Tools" [ref=e92] [cursor=pointer]: + - generic [ref=e95]: + - text: Rendering + - generic [ref=e96]: + - generic [ref=e97]: . + - generic [ref=e98]: . + - generic [ref=e99]: . + - alert [ref=e49] + - navigation [ref=e50]: + - link "aPOS" [ref=e51] [cursor=pointer]: + - /url: / + - generic [ref=e52]: + - link "Tính năng" [ref=e53] [cursor=pointer]: + - /url: /#features + - link "Bảng giá" [ref=e54] [cursor=pointer]: + - /url: /project#pricing + - link "Đăng nhập" [ref=e55] [cursor=pointer]: + - /url: /auth/login + - link "Dùng thử miễn phí" [ref=e56] [cursor=pointer]: + - /url: /register + - main [ref=e57]: + - generic [ref=e58]: + - generic [ref=e59]: + - generic [ref=e60]: + - img [ref=e61] + - text: QUẢN TRỊ + - heading "Đăng nhập Admin" [level=2] [ref=e64] + - paragraph [ref=e65]: Truy cập hệ thống quản trị aPOS + - generic [ref=e66]: + - img [ref=e67] + - generic [ref=e70]: Khu vực bảo mật cao + - generic [ref=e71]: + - generic [ref=e72]: Email quản trị viên + - generic [ref=e73]: + - img [ref=e74] + - textbox "Email quản trị viên" [ref=e77]: admin@goodgo.vn + - generic [ref=e78]: + - generic [ref=e79]: Mật khẩu + - generic [ref=e80]: + - img [ref=e81] + - textbox "Mật khẩu" [ref=e84]: Admin@123 + - button "Đang xử lý..." [disabled] [ref=e100]: + - generic [ref=e86]: Đang xử lý... + - img [ref=e87] + - generic [ref=e89]: + - link "Chi nhánh" [ref=e90] [cursor=pointer]: + - /url: /auth/login/branch + - link "Nhân viên" [ref=e91] [cursor=pointer]: + - /url: /auth/login/staff \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-30-14-605Z.yml b/.playwright-cli/page-2026-05-28T20-30-14-605Z.yml new file mode 100644 index 00000000..46e39f13 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-30-14-605Z.yml @@ -0,0 +1,99 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 03:30 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e21]: + - complementary [ref=e22]: + - generic [ref=e24]: Menu + - generic [ref=e25]: + - link "Karaoke" [ref=e26] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/karaoke + - img [ref=e27] + - generic [ref=e30]: Karaoke + - link "Nhà hàng" [ref=e31] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/restaurant + - img [ref=e32] + - generic [ref=e37]: Nhà hàng + - link "Café" [ref=e38] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e39] + - generic [ref=e41]: Café + - link "Spa" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/spa + - img [ref=e43] + - generic [ref=e46]: Spa + - link "Bán lẻ" [ref=e47] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/retail + - img [ref=e48] + - generic [ref=e51]: Bán lẻ + - link "Quản lý" [ref=e53] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e54] + - generic [ref=e57]: Quản lý + - generic [ref=e58]: + - navigation [ref=e59]: + - button "Bán hàng" [ref=e60] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Bán hàng + - button "Lịch sử" [ref=e64] [cursor=pointer]: + - img [ref=e65] + - generic [ref=e69]: Lịch sử + - button "Báo cáo" [ref=e70] [cursor=pointer]: + - img [ref=e71] + - generic [ref=e73]: Báo cáo + - button "Cài đặt" [ref=e74] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e78]: Cài đặt + - generic [ref=e79]: + - generic [ref=e80]: + - generic [ref=e81]: + - img [ref=e82] + - textbox [ref=e85]: + - /placeholder: Tìm mã đơn, tên khách... + - generic "Bộ lọc lịch sử" [ref=e86]: + - button "Hôm nay" [ref=e87] [cursor=pointer] + - button "7 ngày" [ref=e88] [cursor=pointer] + - button "30 ngày" [ref=e89] [cursor=pointer] + - button "Tất cả" [ref=e90] [cursor=pointer] + - generic [ref=e91]: + - generic [ref=e92]: 2 đơn + - generic [ref=e93]: 2 đã thu + - strong [ref=e94]: 90.000 ₫ + - generic [ref=e95]: Đang tải + - generic [ref=e96]: + - button "2DA66543 Đã thanh toán 1 món 45.000 ₫ 01:28 29-05 Tiền mặt" [ref=e97] [cursor=pointer]: + - generic [ref=e98]: + - generic [ref=e99]: 2DA66543 + - generic [ref=e100]: Đã thanh toán + - generic [ref=e101]: + - generic [ref=e102]: 1 món + - strong [ref=e103]: 45.000 ₫ + - generic [ref=e104]: + - generic [ref=e105]: 01:28 29-05 + - generic [ref=e106]: Tiền mặt + - button "BAABBC27 Đã thanh toán 1 món · A1 45.000 ₫ 01:28 29-05 Tiền mặt" [ref=e107] [cursor=pointer]: + - generic [ref=e108]: + - generic [ref=e109]: BAABBC27 + - generic [ref=e110]: Đã thanh toán + - generic [ref=e111]: + - generic [ref=e112]: 1 món · A1 + - strong [ref=e113]: 45.000 ₫ + - generic [ref=e114]: + - generic [ref=e115]: 01:28 29-05 + - generic [ref=e116]: Tiền mặt + - button "Open Next.js Dev Tools" [ref=e122] [cursor=pointer]: + - img [ref=e123] + - alert [ref=e126] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-30-20-811Z.yml b/.playwright-cli/page-2026-05-28T20-30-20-811Z.yml new file mode 100644 index 00000000..2fd4780a --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-30-20-811Z.yml @@ -0,0 +1,125 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 03:30 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e21]: + - complementary [ref=e22]: + - generic [ref=e24]: Menu + - generic [ref=e25]: + - link "Karaoke" [ref=e26] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/karaoke + - img [ref=e27] + - generic [ref=e30]: Karaoke + - link "Nhà hàng" [ref=e31] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/restaurant + - img [ref=e32] + - generic [ref=e37]: Nhà hàng + - link "Café" [ref=e38] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e39] + - generic [ref=e41]: Café + - link "Spa" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/spa + - img [ref=e43] + - generic [ref=e46]: Spa + - link "Bán lẻ" [ref=e47] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/retail + - img [ref=e48] + - generic [ref=e51]: Bán lẻ + - link "Quản lý" [ref=e53] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e54] + - generic [ref=e57]: Quản lý + - generic [ref=e58]: + - navigation [ref=e59]: + - button "Bán hàng" [ref=e60] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Bán hàng + - button "Lịch sử" [ref=e64] [cursor=pointer]: + - img [ref=e65] + - generic [ref=e69]: Lịch sử + - button "Báo cáo" [ref=e70] [cursor=pointer]: + - img [ref=e71] + - generic [ref=e73]: Báo cáo + - button "Cài đặt" [ref=e74] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e78]: Cài đặt + - generic [ref=e79]: + - generic [ref=e80]: + - generic [ref=e81]: + - generic [ref=e82]: Dashboard bán hàng + - generic [ref=e83]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e84]: + - button "Hôm nay" [ref=e85] [cursor=pointer] + - button "7 ngày" [ref=e86] [cursor=pointer] + - button "30 ngày" [ref=e87] [cursor=pointer] + - generic [ref=e88]: + - generic [ref=e89]: + - generic [ref=e90]: Doanh thu + - strong [ref=e91]: 90.000 ₫ + - generic [ref=e92]: TB 45.000 ₫/đơn + - generic [ref=e93]: + - generic [ref=e94]: Đơn hàng + - strong [ref=e95]: "2" + - generic [ref=e96]: 29/05/2026 · Hôm nay + - generic [ref=e97]: + - generic [ref=e98]: Món bán ra + - strong [ref=e99]: "2" + - generic [ref=e100]: 1.0 món/đơn + - generic [ref=e101]: + - generic [ref=e102]: + - generic [ref=e103]: Món bán chạy + - generic [ref=e105]: + - generic [ref=e106]: Americano + - generic [ref=e107]: 2 đã bán + - generic [ref=e108]: 90.000 ₫ + - generic [ref=e110]: + - generic [ref=e111]: Hình thức thanh toán + - generic [ref=e112]: + - generic [ref=e114]: + - generic [ref=e115]: Tiền mặt + - strong [ref=e116]: 45.000 ₫ + - generic [ref=e120]: + - generic [ref=e121]: Tiền mặt + - strong [ref=e122]: 45.000 ₫ + - generic [ref=e125]: Doanh thu theo giờ + - generic [ref=e126]: + - generic [ref=e129]: 0h + - generic [ref=e132]: 1h + - generic [ref=e135]: 2h + - generic [ref=e138]: 3h + - generic [ref=e141]: 4h + - generic [ref=e144]: 5h + - generic [ref=e147]: 6h + - generic [ref=e150]: 7h + - generic [ref=e153]: 8h + - generic [ref=e156]: 9h + - generic [ref=e159]: 10h + - generic [ref=e162]: 11h + - generic [ref=e165]: 12h + - generic [ref=e168]: 13h + - generic [ref=e171]: 14h + - generic [ref=e174]: 15h + - generic [ref=e177]: 16h + - generic [ref=e180]: 17h + - generic [ref=e183]: 18h + - generic [ref=e186]: 19h + - generic [ref=e189]: 20h + - generic [ref=e192]: 21h + - generic [ref=e195]: 22h + - generic [ref=e198]: 23h + - button "Open Next.js Dev Tools" [ref=e204] [cursor=pointer]: + - img [ref=e205] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-30-58-161Z.yml b/.playwright-cli/page-2026-05-28T20-30-58-161Z.yml new file mode 100644 index 00000000..bfd9e7c9 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-30-58-161Z.yml @@ -0,0 +1,121 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 03:30 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e21]: + - complementary [ref=e22]: + - generic [ref=e24]: Menu + - generic [ref=e25]: + - link "Karaoke" [ref=e26] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/karaoke + - img [ref=e27] + - generic [ref=e30]: Karaoke + - link "Nhà hàng" [ref=e31] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/restaurant + - img [ref=e32] + - generic [ref=e37]: Nhà hàng + - link "Café" [ref=e38] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e39] + - generic [ref=e41]: Café + - link "Spa" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/spa + - img [ref=e43] + - generic [ref=e46]: Spa + - link "Bán lẻ" [ref=e47] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/retail + - img [ref=e48] + - generic [ref=e51]: Bán lẻ + - link "Quản lý" [ref=e53] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e54] + - generic [ref=e57]: Quản lý + - generic [ref=e58]: + - navigation [ref=e59]: + - button "Bán hàng" [ref=e60] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Bán hàng + - button "Lịch sử" [ref=e64] [cursor=pointer]: + - img [ref=e65] + - generic [ref=e69]: Lịch sử + - button "Báo cáo" [ref=e70] [cursor=pointer]: + - img [ref=e71] + - generic [ref=e73]: Báo cáo + - button "Cài đặt" [ref=e74] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e78]: Cài đặt + - generic [ref=e79]: + - generic [ref=e80]: + - generic [ref=e81]: + - generic [ref=e82]: Dashboard bán hàng + - generic [ref=e83]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e84]: + - button "Hôm nay" [ref=e85] [cursor=pointer] + - button "7 ngày" [ref=e86] [cursor=pointer] + - button "30 ngày" [ref=e87] [cursor=pointer] + - generic [ref=e88]: + - generic [ref=e89]: + - generic [ref=e90]: Doanh thu + - strong [ref=e91]: 90.000 ₫ + - generic [ref=e92]: TB 45.000 ₫/đơn + - generic [ref=e93]: + - generic [ref=e94]: Đơn hàng + - strong [ref=e95]: "2" + - generic [ref=e96]: 29/05/2026 · Hôm nay + - generic [ref=e97]: + - generic [ref=e98]: Món bán ra + - strong [ref=e99]: "2" + - generic [ref=e100]: 1.0 món/đơn + - generic [ref=e101]: + - generic [ref=e102]: + - generic [ref=e103]: Món bán chạy + - generic [ref=e105]: + - generic [ref=e106]: Americano + - generic [ref=e107]: 2 đã bán + - generic [ref=e108]: 90.000 ₫ + - generic [ref=e110]: + - generic [ref=e111]: Hình thức thanh toán + - generic [ref=e114]: + - generic [ref=e115]: Tiền mặt + - strong [ref=e116]: 90.000 ₫ + - generic [ref=e119]: Doanh thu theo giờ + - generic [ref=e120]: + - generic [ref=e123]: 0h + - generic [ref=e126]: 1h + - generic [ref=e129]: 2h + - generic [ref=e132]: 3h + - generic [ref=e135]: 4h + - generic [ref=e138]: 5h + - generic [ref=e141]: 6h + - generic [ref=e144]: 7h + - generic [ref=e147]: 8h + - generic [ref=e150]: 9h + - generic [ref=e153]: 10h + - generic [ref=e156]: 11h + - generic [ref=e159]: 12h + - generic [ref=e162]: 13h + - generic [ref=e165]: 14h + - generic [ref=e168]: 15h + - generic [ref=e171]: 16h + - generic [ref=e174]: 17h + - generic [ref=e177]: 18h + - generic [ref=e180]: 19h + - generic [ref=e183]: 20h + - generic [ref=e186]: 21h + - generic [ref=e189]: 22h + - generic [ref=e192]: 23h + - button "Open Next.js Dev Tools" [ref=e198] [cursor=pointer]: + - img [ref=e199] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-31-13-642Z.yml b/.playwright-cli/page-2026-05-28T20-31-13-642Z.yml new file mode 100644 index 00000000..ad1a901e --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-31-13-642Z.yml @@ -0,0 +1,121 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: QA Cafe 282591 + - generic [ref=e10]: + - generic [ref=e11]: Online + - generic [ref=e13]: + - img [ref=e14] + - text: 03:31 + - link [ref=e17] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e18] + - generic [ref=e21]: + - complementary [ref=e22]: + - generic [ref=e24]: Menu + - generic [ref=e25]: + - link "Karaoke" [ref=e26] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/karaoke + - img [ref=e27] + - generic [ref=e30]: Karaoke + - link "Nhà hàng" [ref=e31] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/restaurant + - img [ref=e32] + - generic [ref=e37]: Nhà hàng + - link "Café" [ref=e38] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e39] + - generic [ref=e41]: Café + - link "Spa" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/spa + - img [ref=e43] + - generic [ref=e46]: Spa + - link "Bán lẻ" [ref=e47] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/retail + - img [ref=e48] + - generic [ref=e51]: Bán lẻ + - link "Quản lý" [ref=e53] [cursor=pointer]: + - /url: /admin/shop/8d99d966-883e-4806-b247-ee940e6a779c/overview + - img [ref=e54] + - generic [ref=e57]: Quản lý + - generic [ref=e58]: + - navigation [ref=e59]: + - button "Bán hàng" [ref=e60] [cursor=pointer]: + - img [ref=e61] + - generic [ref=e63]: Bán hàng + - button "Lịch sử" [ref=e64] [cursor=pointer]: + - img [ref=e65] + - generic [ref=e69]: Lịch sử + - button "Báo cáo" [ref=e70] [cursor=pointer]: + - img [ref=e71] + - generic [ref=e73]: Báo cáo + - button "Cài đặt" [ref=e74] [cursor=pointer]: + - img [ref=e75] + - generic [ref=e78]: Cài đặt + - generic [ref=e79]: + - generic [ref=e80]: + - generic [ref=e81]: + - generic [ref=e82]: Dashboard bán hàng + - generic [ref=e83]: 29/05/2026 · Hôm nay + - generic "Khoảng thời gian dashboard" [ref=e84]: + - button "Hôm nay" [ref=e85] [cursor=pointer] + - button "7 ngày" [ref=e86] [cursor=pointer] + - button "30 ngày" [ref=e87] [cursor=pointer] + - generic [ref=e88]: + - generic [ref=e89]: + - generic [ref=e90]: Doanh thu + - strong [ref=e91]: 90.000 ₫ + - generic [ref=e92]: TB 45.000 ₫/đơn + - generic [ref=e93]: + - generic [ref=e94]: Đơn hàng + - strong [ref=e95]: "2" + - generic [ref=e96]: 29/05/2026 · Hôm nay + - generic [ref=e97]: + - generic [ref=e98]: Món bán ra + - strong [ref=e99]: "2" + - generic [ref=e100]: 1.0 món/đơn + - generic [ref=e101]: + - generic [ref=e102]: + - generic [ref=e103]: Món bán chạy + - generic [ref=e105]: + - generic [ref=e106]: Americano + - generic [ref=e107]: 2 đã bán + - generic [ref=e108]: 90.000 ₫ + - generic [ref=e110]: + - generic [ref=e111]: Hình thức thanh toán + - generic [ref=e114]: + - generic [ref=e115]: Tiền mặt + - strong [ref=e116]: 90.000 ₫ + - generic [ref=e119]: Doanh thu theo giờ + - generic [ref=e120]: + - generic [ref=e123]: 0h + - generic [ref=e126]: 1h + - generic [ref=e129]: 2h + - generic [ref=e132]: 3h + - generic [ref=e135]: 4h + - generic [ref=e138]: 5h + - generic [ref=e141]: 6h + - generic [ref=e144]: 7h + - generic [ref=e147]: 8h + - generic [ref=e150]: 9h + - generic [ref=e153]: 10h + - generic [ref=e156]: 11h + - generic [ref=e159]: 12h + - generic [ref=e162]: 13h + - generic [ref=e165]: 14h + - generic [ref=e168]: 15h + - generic [ref=e171]: 16h + - generic [ref=e174]: 17h + - generic [ref=e177]: 18h + - generic [ref=e180]: 19h + - generic [ref=e183]: 20h + - generic [ref=e186]: 21h + - generic [ref=e189]: 22h + - generic [ref=e192]: 23h + - button "Open Next.js Dev Tools" [ref=e198] [cursor=pointer]: + - img [ref=e199] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-31-31-650Z.yml b/.playwright-cli/page-2026-05-28T20-31-31-650Z.yml new file mode 100644 index 00000000..74038df1 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-31-31-650Z.yml @@ -0,0 +1,183 @@ +- generic [active] [ref=e1]: + - main [ref=e2]: + - generic [ref=e3]: + - generic [ref=e4]: + - link [ref=e5] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe + - img [ref=e6] + - generic [ref=e8]: aPOS POS + - generic [ref=e9]: Giảm giá + - generic [ref=e11]: Online + - generic [ref=e12]: + - complementary [ref=e13]: + - link "Barista queue" [ref=e14] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/barista-queue + - img [ref=e15] + - generic [ref=e17]: Barista queue + - link "Stamp card" [ref=e18] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/loyalty-stamp + - img [ref=e19] + - generic [ref=e23]: Stamp card + - link "Daily report" [ref=e24] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/daily-report + - img [ref=e25] + - generic [ref=e27]: Daily report + - link "Customer display" [ref=e28] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/queue-display + - img [ref=e29] + - generic [ref=e31]: Customer display + - link "Menu management" [ref=e32] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/menu-management + - img [ref=e33] + - generic [ref=e36]: Menu management + - link "Order customize" [ref=e37] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/order-customize + - img [ref=e38] + - generic [ref=e41]: Order customize + - link "Milk foam options" [ref=e42] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/milk-foam-options + - img [ref=e43] + - generic [ref=e45]: Milk foam options + - link "Cafe journey" [ref=e46] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/cafe-journey + - img [ref=e47] + - generic [ref=e50]: Cafe journey + - link "Chọn phương thức" [ref=e51] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/method-select + - img [ref=e52] + - generic [ref=e54]: Chọn phương thức + - link "Thanh toán tiền mặt" [ref=e55] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/cash + - img [ref=e56] + - generic [ref=e58]: Thanh toán tiền mặt + - link "Thanh toán thẻ" [ref=e59] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/card + - img [ref=e60] + - generic [ref=e62]: Thanh toán thẻ + - link "Thanh toán QR" [ref=e63] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/qr + - img [ref=e64] + - generic [ref=e66]: Thanh toán QR + - link "Chuyển khoản" [ref=e67] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/bank-transfer + - img [ref=e68] + - generic [ref=e71]: Chuyển khoản + - link "Gift card" [ref=e72] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/gift-card + - img [ref=e73] + - generic [ref=e77]: Gift card + - link "Thanh toán một phần" [ref=e78] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/partial + - img [ref=e79] + - generic [ref=e81]: Thanh toán một phần + - link "Chờ thanh toán" [ref=e82] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/pending + - img [ref=e83] + - generic [ref=e86]: Chờ thanh toán + - link "Thanh toán thành công" [ref=e87] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/payment/success + - img [ref=e88] + - generic [ref=e91]: Thanh toán thành công + - link "Sửa đơn" [ref=e92] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/order-edit + - img [ref=e93] + - generic [ref=e96]: Sửa đơn + - link "Giảm giá" [ref=e97] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/discount + - img [ref=e98] + - generic [ref=e101]: Giảm giá + - link "Chọn khách hàng" [ref=e102] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/customer + - img [ref=e103] + - generic [ref=e105]: Chọn khách hàng + - link "Chuyển bàn/phòng" [ref=e106] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/table-transfer + - img [ref=e107] + - generic [ref=e109]: Chuyển bàn/phòng + - link "Kiểm kho" [ref=e110] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/stock-out + - img [ref=e111] + - generic [ref=e114]: Kiểm kho + - link "Tìm món/SKU" [ref=e115] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/price-check + - img [ref=e116] + - generic [ref=e120]: Tìm món/SKU + - link "Két tiền" [ref=e121] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/operations/cash-drawer + - img [ref=e122] + - generic [ref=e124]: Két tiền + - link "Ca làm" [ref=e125] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/operations/shift + - img [ref=e126] + - generic [ref=e130]: Ca làm + - link "Đơn chờ xử lý" [ref=e131] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/operations/pending-orders + - img [ref=e132] + - generic [ref=e135]: Đơn chờ xử lý + - link "Bán nhanh" [ref=e136] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/operations/quick-sale + - img [ref=e137] + - generic [ref=e139]: Bán nhanh + - link "Tách hóa đơn" [ref=e140] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/split-bill + - img [ref=e141] + - generic [ref=e143]: Tách hóa đơn + - link "Hủy / hoàn tiền" [ref=e144] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/dialog/void-refund + - img [ref=e145] + - generic [ref=e148]: Hủy / hoàn tiền + - link "Tablet POS" [ref=e149] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/tablet + - img [ref=e150] + - generic [ref=e152]: Tablet POS + - link "Mobile POS" [ref=e153] [cursor=pointer]: + - /url: /pos/8d99d966-883e-4806-b247-ee940e6a779c/cafe/mobile + - img [ref=e154] + - generic [ref=e156]: Mobile POS + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e159] + - generic [ref=e162]: + - generic [ref=e163]: CAFE WORKFLOW + - heading "Giảm giá" [level=1] [ref=e164] + - paragraph [ref=e165]: Áp voucher, khuyến mãi hoặc chiết khấu thủ công. + - generic [ref=e166]: + - article [ref=e167]: + - generic [ref=e168]: Catalog service + - strong [ref=e169]: "4" + - generic [ref=e170]: Sản phẩm + - article [ref=e171]: + - generic [ref=e172]: FnB service + - strong [ref=e173]: "4" + - generic [ref=e174]: Bàn/phòng + - article [ref=e175]: + - generic [ref=e176]: Order service + - strong [ref=e177]: "2" + - generic [ref=e178]: Đơn hàng + - article [ref=e179]: + - generic [ref=e180]: Payment ledger + - strong [ref=e181]: 90.000 ₫ + - generic [ref=e182]: Doanh thu + - generic [ref=e183]: + - generic [ref=e184]: + - text: GIẢM GIÁ + - heading "Kiểm tra voucher thật" [level=2] [ref=e185] + - paragraph [ref=e186]: Workflow này gọi BFF voucher validate theo shop. Việc áp vào hóa đơn chỉ bật khi có service chỉnh sửa đơn persisted. + - generic [ref=e187]: + - generic [ref=e188]: Mã voucher + - textbox "Mã voucher" [ref=e189]: + - /placeholder: Nhập mã voucher + - button "Kiểm tra voucher" [ref=e190] [cursor=pointer]: + - img [ref=e191] + - text: Kiểm tra voucher + - generic [ref=e193]: + - article [ref=e194]: + - strong [ref=e195]: 2DA66543 + - generic [ref=e196]: 1 món · Paid + - generic [ref=e197]: 45.000 ₫ + - article [ref=e198]: + - strong [ref=e199]: Bàn/phòng A1 + - generic [ref=e200]: 1 món · Paid + - generic [ref=e201]: 45.000 ₫ + - button "Open Next.js Dev Tools" [ref=e207] [cursor=pointer]: + - img [ref=e208] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-32-31-730Z.yml b/.playwright-cli/page-2026-05-28T20-32-31-730Z.yml new file mode 100644 index 00000000..e7635d02 --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-32-31-730Z.yml @@ -0,0 +1,44 @@ +- generic [active] [ref=e1]: + - navigation [ref=e2]: + - link "aPOS" [ref=e3] [cursor=pointer]: + - /url: / + - generic [ref=e4]: + - link "Tính năng" [ref=e5] [cursor=pointer]: + - /url: /#features + - link "Bảng giá" [ref=e6] [cursor=pointer]: + - /url: /project#pricing + - link "Đăng nhập" [ref=e7] [cursor=pointer]: + - /url: /auth/login + - link "Dùng thử miễn phí" [ref=e8] [cursor=pointer]: + - /url: /register + - main [ref=e9]: + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: + - img [ref=e13] + - text: QUẢN TRỊ + - heading "Đăng nhập Admin" [level=2] [ref=e16] + - paragraph [ref=e17]: Truy cập hệ thống quản trị aPOS + - generic [ref=e18]: + - img [ref=e19] + - generic [ref=e22]: Khu vực bảo mật cao + - generic [ref=e23]: + - generic [ref=e24]: Email quản trị viên + - generic [ref=e25]: + - img [ref=e26] + - textbox "Email quản trị viên" [ref=e29] + - generic [ref=e30]: + - generic [ref=e31]: Mật khẩu + - generic [ref=e32]: + - img [ref=e33] + - textbox "Mật khẩu" [ref=e36] + - button "Đăng nhập bảo mật" [ref=e37] [cursor=pointer]: + - generic [ref=e38]: Đăng nhập bảo mật + - img [ref=e39] + - generic [ref=e41]: + - link "Chi nhánh" [ref=e42] [cursor=pointer]: + - /url: /auth/login/branch + - link "Nhân viên" [ref=e43] [cursor=pointer]: + - /url: /auth/login/staff + - button "Open Next.js Dev Tools" [ref=e49] [cursor=pointer]: + - img [ref=e50] \ No newline at end of file diff --git a/.playwright-cli/page-2026-05-28T20-34-49-893Z.png b/.playwright-cli/page-2026-05-28T20-34-49-893Z.png new file mode 100644 index 00000000..64382191 Binary files /dev/null and b/.playwright-cli/page-2026-05-28T20-34-49-893Z.png differ diff --git a/.playwright-cli/page-2026-05-28T20-44-31-612Z.yml b/.playwright-cli/page-2026-05-28T20-44-31-612Z.yml new file mode 100644 index 00000000..c3cd89dc --- /dev/null +++ b/.playwright-cli/page-2026-05-28T20-44-31-612Z.yml @@ -0,0 +1,45 @@ +- generic [active] [ref=e1]: + - navigation [ref=e2]: + - link "aPOS" [ref=e3] [cursor=pointer]: + - /url: / + - generic [ref=e4]: + - link "Tính năng" [ref=e5] [cursor=pointer]: + - /url: /#features + - link "Bảng giá" [ref=e6] [cursor=pointer]: + - /url: /project#pricing + - link "Đăng nhập" [ref=e7] [cursor=pointer]: + - /url: /auth/login + - link "Dùng thử miễn phí" [ref=e8] [cursor=pointer]: + - /url: /register + - main [ref=e9]: + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: + - img [ref=e13] + - text: QUẢN TRỊ + - heading "Đăng nhập Admin" [level=2] [ref=e16] + - paragraph [ref=e17]: Truy cập hệ thống quản trị aPOS + - generic [ref=e18]: + - img [ref=e19] + - generic [ref=e22]: Khu vực bảo mật cao + - generic [ref=e23]: + - generic [ref=e24]: Email quản trị viên + - generic [ref=e25]: + - img [ref=e26] + - textbox "Email quản trị viên" [ref=e29] + - generic [ref=e30]: + - generic [ref=e31]: Mật khẩu + - generic [ref=e32]: + - img [ref=e33] + - textbox "Mật khẩu" [ref=e36] + - button "Đăng nhập bảo mật" [ref=e37] [cursor=pointer]: + - generic [ref=e38]: Đăng nhập bảo mật + - img [ref=e39] + - generic [ref=e41]: + - link "Chi nhánh" [ref=e42] [cursor=pointer]: + - /url: /auth/login/branch + - link "Nhân viên" [ref=e43] [cursor=pointer]: + - /url: /auth/login/staff + - button "Open Next.js Dev Tools" [ref=e49] [cursor=pointer]: + - img [ref=e50] + - alert [ref=e53] \ No newline at end of file diff --git a/microservices/apps/tpos-mvp-next/src/app/admin/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/admin/[...path]/page.tsx index 5246ccd7..4d3f128b 100644 --- a/microservices/apps/tpos-mvp-next/src/app/admin/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/admin/[...path]/page.tsx @@ -1,11 +1,14 @@ import { notFound, redirect } from "next/navigation"; import { + AdminSectionView, AdminNotFoundView, + ShopFinanceView, + ShopHistoryView, ShopOverviewView, StoreCreateWizard, StoreListView } from "@/components/admin/AdminReferenceViews"; -import { TposPortal, buildPortalPayload } from "@/components/TposPortal"; +import { filterPortalShops, requirePortalRole } from "@/server/auth/portal"; import { getDashboardStats } from "@/server/db/queries"; import { getShopService, getShopStatsService, listShopsService } from "@/server/services/shop"; import { listCatalogCategoriesByShop, listCatalogProductsByShop } from "@/server/services/catalog"; @@ -32,6 +35,8 @@ import { listStaff, listTherapists, listUsers, + listWallets, + listWalletTransactions, reportRevenue, reportTopProducts } from "@/server/services/parity"; @@ -42,6 +47,7 @@ export const dynamic = "force-dynamic"; export default async function AdminCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { const path = (await params).path ?? []; + const user = await requirePortalRole(["admin"], `/admin/${path.join("/")}`, path[0] === "shop" || path[0] === "store" ? path[1] : null); if (path[0] === "store" && path[1]) { if (path.length !== 3 || path[2] !== "stock") notFound(); redirect(`/admin/shop/${path[1]}/${path[2] === "stock" ? "inventory" : path.slice(2).join("/") || "overview"}`); @@ -54,7 +60,9 @@ export default async function AdminCatchAllPage({ params }: { params: Promise<{ if (path[0] === "stores" && !path[1]) { const [shops, shopStats] = await Promise.all([listShopsService(), getShopStatsService()]); - return ; + const visibleShops = filterPortalShops(user, shops); + const visibleIds = new Set(visibleShops.map((item) => item.id)); + return visibleIds.has(item.shopId))} />; } const isShopRoute = path[0] === "shop"; @@ -64,7 +72,8 @@ export default async function AdminCatchAllPage({ params }: { params: Promise<{ return ; } const scopedShop = isShopRoute || path.length === 0 ? shop : null; - const stats = await getDashboardStats(scopedShop?.id); + const allowedShopIds = allowedShopIdsForUser(user); + const stats = await getAdminScopedStats(scopedShop?.id, allowedShopIds); const section = isShopRoute ? path.slice(2).join("/") || "overview" : path.join("/") || "dashboard"; if (!isKnownAdminSection(section, isShopRoute ? shop : null)) notFound(); @@ -75,36 +84,59 @@ export default async function AdminCatchAllPage({ params }: { params: Promise<{ if (isShopRoute && section === "overview" && shop) { const [overviewStats, orders, products, tables, staff, appointments] = await Promise.all([ getDashboardStats(shop.id), - listOrdersService({ shopId: shop.id, page: 1, pageSize: 24, filter: "all" }), + listOrdersService({ shopId: shop.id, page: 1, pageSize: 500, filter: "all" }), listCatalogProductsByShop(shop.id), listTablesByShop(shop.id), listStaff(shop.id), listAppointments(shop.id) ]); - return ; + return [0]["payload"]["appointments"] }} />; } - const items = await loadItems(section, isShopRoute ? shop : null); + if (isShopRoute && (section === "history" || section === "orders") && shop) { + const orders = await listOrdersService({ shopId: shop.id, page: 1, pageSize: 500, filter: "all" }); + return ; + } - return ( - - ); + if (isShopRoute && section === "finance" && shop) { + const [orders, wallets, walletTransactions, revenueRows, topProducts] = await Promise.all([ + listOrdersService({ shopId: shop.id, page: 1, pageSize: 500, filter: "all" }), + listWallets(user.id), + listWalletTransactions(user.id, 25), + scopedRevenueRows(shop.id, allowedShopIds), + scopedTopProductRows(shop.id, allowedShopIds) + ]); + return [0]["payload"]["wallets"], + walletTransactions: walletTransactions as unknown as Parameters[0]["payload"]["walletTransactions"], + revenueRows: revenueRows as unknown as Parameters[0]["payload"]["revenueRows"], + topProducts: topProducts as unknown as Parameters[0]["payload"]["topProducts"] + }} />; + } + + const items = await loadItems(section, isShopRoute ? shop : null, allowedShopIds); + + return ; } -async function loadItems(section: string, shop?: Shop | null) { +async function loadItems(section: string, shop?: Shop | null, allowedShopIds?: string[] | null) { const shopId = shop?.id; + if (section === "dashboard") { + const [shops, stats] = await Promise.all([listShopsService(), getShopStatsService()]); + const visible = shops.filter((item) => allowedShopIds == null || allowedShopIds.includes(item.id)); + const visibleIds = new Set(visible.map((item) => item.id)); + return [ + ...visible.map((item) => ({ title: item.name, meta: item.category, value: item.status, href: `/admin/shop/${item.id}/overview` })), + ...stats.filter((item) => visibleIds.has(item.shopId)).map((item) => ({ title: `Doanh thu ${item.shopId.slice(0, 8)}`, meta: `${item.todayOrderCount} đơn hôm nay`, value: formatMoney(item.monthRevenue) })) + ]; + } if (section === "stores") { const shops = await listShopsService(); - return shops.map((item) => ({ title: item.name, meta: item.category, value: item.status, href: `/admin/shop/${item.id}/overview` })); + return shops + .filter((item) => allowedShopIds == null || allowedShopIds.includes(item.id)) + .map((item) => ({ title: item.name, meta: item.category, value: item.status, href: `/admin/shop/${item.id}/overview` })); } if (section === "system/audit") { const logs = await auditLogs(24); @@ -115,6 +147,14 @@ async function loadItems(section: string, shop?: Shop | null) { })); } if (section === "users") { + if (shopId || allowedShopIds !== null) { + const staff = await scopedStaffRows(shopId, allowedShopIds); + return staff.map((item) => ({ + title: `${item.first_name ?? "Nhân viên"} ${item.last_name ?? ""}`.trim(), + meta: String(item.email ?? item.phone ?? item.employee_code ?? "Staff"), + value: String(item.role ?? item.status ?? "active") + })); + } const users = await listUsers(); return users.map((user) => ({ title: String(user.displayName), @@ -126,6 +166,21 @@ async function loadItems(section: string, shop?: Shop | null) { const roles = await listRoles(); return roles.map((role) => ({ title: role.name, meta: role.portal, value: role.code })); } + if (section === "settings" && shop) { + const hours = shop.openTime && shop.closeTime ? `${shop.openTime} - ${shop.closeTime}` : "Chưa cấu hình"; + const activeDays = shop.activeDays?.length ? shop.activeDays.join(", ") : "Chưa cấu hình"; + const address = [shop.address, shop.district, shop.city].filter(Boolean).join(", ") || "Chưa cấu hình"; + return [ + { title: shop.name, meta: shop.email ?? shop.phone ?? "Chưa có liên hệ", value: shop.status }, + { title: "Ngành vận hành", meta: shop.category, value: normalizeVertical(shop.vertical), href: `/pos/${shop.id}/${normalizeVertical(shop.vertical)}` }, + { title: "Địa chỉ", meta: address, value: "Location" }, + { title: "Giờ mở cửa", meta: `${hours} · ${activeDays}`, value: "Store hours" }, + { title: "Tính năng ngành", meta: "QR ordering, POS terminal, kitchen/barista, inventory", value: "Enabled" }, + { title: "Mẫu hóa đơn", meta: "Logo, footer, thuế/phí, phiếu bếp", value: "Template", href: `/admin/shop/${shop.id}/receipt-templates` }, + { title: "AI assistant", meta: "Provider, system prompt, local tool calls", value: "Config", href: `/admin/shop/${shop.id}/ai-chat` }, + { title: "Menu khách hàng", meta: "QR ordering", value: "Public", href: `/menu/${shop.id}` } + ]; + } if (section === "settings") { return [ { title: "Cài đặt hệ thống", meta: "Thông tin tài khoản, gói dịch vụ và thông báo", value: "Admin" }, @@ -146,17 +201,6 @@ async function loadItems(section: string, shop?: Shop | null) { { title: "Social", meta: "Facebook, Zalo, WhatsApp, X", value: "Env required" } ]; } - if (section === "settings" && shop) { - return [ - { title: shop.name, meta: shop.email ?? shop.phone ?? "Chưa có liên hệ", value: shop.status }, - { title: "Ngành vận hành", meta: shop.category, value: normalizeVertical(shop.vertical), href: `/pos/${shop.id}/${normalizeVertical(shop.vertical)}` }, - { title: "Giờ mở cửa", meta: "08:00 - 22:00 · tất cả ngày bán", value: "Store hours" }, - { title: "Tính năng ngành", meta: "QR ordering, POS terminal, kitchen/barista, inventory", value: "Enabled" }, - { title: "Mẫu hóa đơn", meta: "Logo, footer, thuế/phí, phiếu bếp", value: "Template", href: `/admin/shop/${shop.id}/receipt-templates` }, - { title: "AI assistant", meta: "Provider, system prompt, local tool calls", value: "Config", href: `/admin/shop/${shop.id}/ai-chat` }, - { title: "Menu khách hàng", meta: "QR ordering", value: "Public", href: `/menu/${shop.id}` } - ]; - } if (section === "qr-codes" && shopId) { const tables = await listTablesByShop(shopId); return tables.map((table) => ({ @@ -185,6 +229,15 @@ async function loadItems(section: string, shop?: Shop | null) { const tables = await listTablesByShop(shopId); return tables.map((table) => ({ title: `${section === "rooms" ? "Phòng" : "Bàn"} ${table.tableNumber}`, meta: `${table.zone ?? "Khu chính"} · ${table.capacity} chỗ`, value: table.status, href: `/admin/shop/${shopId}/${section}` })); } + if ((section === "history" || section === "orders") && shopId) { + const orders = await listOrdersService({ shopId, page: 1, pageSize: 100, filter: "all" }); + return orders.items.map((order) => ({ + title: `#${order.id.slice(0, 8).toUpperCase()}`, + meta: `${order.itemCount} món · ${new Date(order.createdAt).toLocaleString("vi-VN")}`, + value: formatMoney(order.totalAmount), + href: `/pos/${shopId}/${normalizeVertical(shop?.vertical)}?tab=history` + })); + } if (section === "zones" && shopId) { const tables = await listTablesByShop(shopId); const zones = new Map(); @@ -230,9 +283,8 @@ async function loadItems(section: string, shop?: Shop | null) { return members.map((member) => ({ title: String(member.display_name ?? "Khách hàng"), meta: String(member.phone ?? member.level_name ?? ""), value: `Level ${member.current_level}` })); } if (section === "promotions" || section === "happy-hour") { - const campaigns = await listCampaigns(); + const campaigns = await listCampaigns(shopId); return campaigns - .filter((item) => !shopId || String(item.shop_id ?? "") === shopId) .map((item) => ({ title: String(item.name), meta: String(item.description ?? "Campaign"), value: String(item.status) })); } if (section === "appointments" || section === "spa/appointments") { @@ -248,19 +300,35 @@ async function loadItems(section: string, shop?: Shop | null) { return resources.map((item) => ({ title: String(item.name), meta: String(item.resource_type ?? "resource"), value: `${item.capacity ?? 1}` })); } if (section === "finance" && shopId) { - const [revenue, products] = await Promise.all([reportRevenue(shopId), reportTopProducts(shopId)]); + const [revenue, products] = await Promise.all([scopedRevenueRows(shopId, allowedShopIds), scopedTopProductRows(shopId, allowedShopIds)]); return [ ...revenue.slice(0, 6).map((row) => ({ title: String(row.day), meta: `${row.order_count} đơn`, value: formatMoney(Number(row.revenue ?? 0)) })), ...products.slice(0, 6).map((row) => ({ title: String(row.product_name), meta: `${row.quantity_sold ?? 0} bán`, value: formatMoney(Number(row.revenue ?? 0)) })) ]; } - if (section === "reports" || section === "reports/eod") { - const [revenue, products] = await Promise.all([reportRevenue(shopId), reportTopProducts(shopId)]); + if (section === "reports" || section === "reports/eod" || section === "reports/revenue") { + const [revenue, products] = await Promise.all([scopedRevenueRows(shopId, allowedShopIds), scopedTopProductRows(shopId, allowedShopIds)]); return [ ...revenue.slice(0, 8).map((row) => ({ title: String(row.day), meta: `${row.order_count} đơn`, value: formatMoney(Number(row.revenue ?? 0)) })), ...products.slice(0, 6).map((row) => ({ title: String(row.product_name), meta: `${row.quantity_sold ?? 0} bán`, value: formatMoney(Number(row.revenue ?? 0)) })) ]; } + if (section === "reports/staff") { + const staff = await scopedStaffRows(shopId, allowedShopIds); + return staff.map((item) => ({ title: `${item.first_name ?? "Nhân viên"} ${item.last_name ?? ""}`, meta: String(item.employee_code ?? item.role ?? "Staff"), value: String(item.status ?? "active") })); + } + if (section.startsWith("onboarding/")) { + return onboardingItems(section); + } + if (section === "spa/therapists") { + const shops = await listShopsService(); + const scopedShops = shops.filter((item) => (allowedShopIds == null || allowedShopIds.includes(item.id)) && normalizeVertical(item.vertical) === "spa"); + const rows = (await Promise.all(scopedShops.map(async (item) => { + const therapists = await listTherapists(item.id); + return therapists.map((therapist) => ({ title: String(therapist.name), meta: `${item.name} · ${String(therapist.specialty ?? "Therapist")}`, value: String(therapist.status ?? "active") })); + }))).flat(); + return rows.length ? rows : [{ title: "Chưa có therapist", meta: "Thêm nhân sự spa trong từng cửa hàng", value: "Empty" }]; + } if (section === "returns") { return [ { title: "Đổi hàng", meta: "Kiểm bill gốc, tồn kho và bù trừ", value: "Ready" }, @@ -280,7 +348,7 @@ async function loadItems(section: string, shop?: Shop | null) { ]; } if (section === "drive" && shopId) { - const [files, folders] = await Promise.all([listFiles(shopId), listFolders()]); + const [files, folders] = await Promise.all([listFiles(shopId), listFolders(shopId)]); return [ ...folders.map((folder) => ({ title: String(folder.name), meta: "Thư mục", value: "Folder" })), ...files.map((file) => ({ title: String(file.file_name), meta: String(file.content_type ?? "file"), value: `${Math.round(Number(file.byte_size ?? 0) / 1024)} KB` })) @@ -322,6 +390,8 @@ function adminTitle(section: string) { kitchen: "Bếp", recipes: "Công thức", shifts: "Ca bán", + history: "Lịch sử đơn", + orders: "Lịch sử đơn", finance: "Tài chính", staff: "Nhân sự", attendance: "Điểm danh", @@ -355,6 +425,45 @@ function normalizeVertical(value?: string | null) { return value === "restaurant" || value === "karaoke" || value === "spa" || value === "beauty" || value === "retail" ? value : "cafe"; } +function allowedShopIdsForUser(user: Awaited>) { + return user.roles.some((role) => role.code === "superadmin") ? null : user.roles.map((role) => role.shop_id).filter(Boolean) as string[]; +} + +async function getAdminScopedStats(shopId?: string | null, allowedShopIds?: string[] | null) { + if (shopId || allowedShopIds == null) return getDashboardStats(shopId); + if (allowedShopIds.length === 0) { + return { + shopCount: 0, + activeShopCount: 0, + productCount: 0, + orderCount: 0, + todayRevenue: 0, + monthRevenue: 0, + lowStockCount: 0, + tableCount: 0, + recentOrders: [], + lowStock: [] + }; + } + + const [shops, shopStats] = await Promise.all([listShopsService(), getShopStatsService()]); + const allowed = new Set(allowedShopIds); + const visibleShops = shops.filter((item) => allowed.has(item.id)); + const visibleStats = shopStats.filter((item) => allowed.has(item.shopId)); + return { + shopCount: visibleShops.length, + activeShopCount: visibleShops.filter((item) => item.statusId === 2 || item.status.toLowerCase() === "active").length, + productCount: visibleStats.reduce((sum, item) => sum + item.productCount, 0), + orderCount: visibleStats.reduce((sum, item) => sum + item.orderCount, 0), + todayRevenue: visibleStats.reduce((sum, item) => sum + item.revenue, 0), + monthRevenue: visibleStats.reduce((sum, item) => sum + item.monthRevenue, 0), + lowStockCount: 0, + tableCount: 0, + recentOrders: [], + lowStock: [] + }; +} + function isKnownAdminSection(section: string, shop?: Shop | null) { if (shop) { const vertical = normalizeVertical(shop.vertical) as VerticalKind; @@ -363,6 +472,8 @@ function isKnownAdminSection(section: string, shop?: Shop | null) { "products", "combos", "doctors", + "history", + "orders", "qr-codes" ]); return sidebarSections.has(section as never) || dataSections.has(section); @@ -386,6 +497,45 @@ function isKnownAdminSection(section: string, shop?: Shop | null) { return globalSections.has(section); } +async function scopedRevenueRows(shopId?: string | null, allowedShopIds?: string[] | null) { + if (shopId) return reportRevenue(shopId); + if (allowedShopIds == null) return reportRevenue(null); + if (allowedShopIds.length === 0) return []; + return (await Promise.all(allowedShopIds.map((id) => reportRevenue(id)))).flat(); +} + +async function scopedTopProductRows(shopId?: string | null, allowedShopIds?: string[] | null) { + if (shopId) return reportTopProducts(shopId); + if (allowedShopIds == null) return reportTopProducts(null); + if (allowedShopIds.length === 0) return []; + return (await Promise.all(allowedShopIds.map((id) => reportTopProducts(id)))).flat(); +} + +async function scopedStaffRows(shopId?: string | null, allowedShopIds?: string[] | null) { + if (shopId) return listStaff(shopId); + if (allowedShopIds == null) return listStaff(null); + if (allowedShopIds.length === 0) return []; + return (await Promise.all(allowedShopIds.map((id) => listStaff(id)))).flat(); +} + +function onboardingItems(section: string) { + const steps = [ + ["onboarding/business", "Thông tin doanh nghiệp", "Tên pháp lý, MST, ngành kinh doanh"], + ["onboarding/store", "Cửa hàng đầu tiên", "Địa chỉ, giờ mở cửa và cấu hình ngành"], + ["onboarding/products", "Menu & sản phẩm", "Danh mục, giá bán và tồn đầu kỳ"], + ["onboarding/staff", "Nhân sự", "Tài khoản staff, ca làm và phân quyền"], + ["onboarding/device", "Thiết bị", "POS terminal, máy in, KDS và QR ordering"], + ["onboarding/ready", "Sẵn sàng vận hành", "Mở POS, test đơn hàng và bàn giao"] + ]; + const currentIndex = Math.max(0, steps.findIndex(([slug]) => slug === section)); + return steps.map(([slug, title, meta], index) => ({ + title, + meta, + value: index < currentIndex ? "Done" : index === currentIndex ? "Current" : "Next", + href: `/admin/${slug}` + })); +} + function formatMoney(value: number) { return new Intl.NumberFormat("vi-VN", { style: "currency", currency: "VND", maximumFractionDigits: 0 }).format(value); } diff --git a/microservices/apps/tpos-mvp-next/src/app/admin/page.tsx b/microservices/apps/tpos-mvp-next/src/app/admin/page.tsx index 5444dae6..5af5fd17 100644 --- a/microservices/apps/tpos-mvp-next/src/app/admin/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/admin/page.tsx @@ -1,11 +1,15 @@ import { AdminDashboardView } from "@/components/admin/AdminReferenceViews"; +import { filterPortalShops, requirePortalRole } from "@/server/auth/portal"; import { getShopStatsService, listShopsService } from "@/server/services/shop"; export const dynamic = "force-dynamic"; export default async function AdminPage() { + const user = await requirePortalRole(["admin"], "/admin"); const [shops, shopStats] = await Promise.all([listShopsService(), getShopStatsService()]); - return ; + const visibleShops = filterPortalShops(user, shops); + const visibleIds = new Set(visibleShops.map((shop) => shop.id)); + return visibleIds.has(stat.shopId))} serviceHealth={defaultServiceHealth()} />; } function defaultServiceHealth() { diff --git a/microservices/apps/tpos-mvp-next/src/app/api/bff/[...path]/route.ts b/microservices/apps/tpos-mvp-next/src/app/api/bff/[...path]/route.ts index 678d503a..9eef3b02 100644 --- a/microservices/apps/tpos-mvp-next/src/app/api/bff/[...path]/route.ts +++ b/microservices/apps/tpos-mvp-next/src/app/api/bff/[...path]/route.ts @@ -15,6 +15,7 @@ import { } from "@/server/services/catalog"; import { createTableService, + getTable, getTableFromToken, listTablesByShop, removeTableService, @@ -54,6 +55,7 @@ import { } from "@/server/services/shop"; import { addExperience, + assignUserShopRole, auditLogs, baristaStats, checkIn, @@ -69,17 +71,24 @@ import { createStaff, deleteFileRecord, deleteFolder, - deleteMember, - deleteStaff, - getCampaign, - getFileRecord, - getAiConfig, - getAttendance, + deleteMember, + deleteStaff, + getAppointment, + getBaristaQueueItem, + getCampaign, + getFileRecord, + getFolder, + getAiConfig, + getAttendance, + getKitchenTicket, + getLeaveRequest, getMember, - getMemberProgress, - getMembershipLevel, + getMemberProgress, + getMembershipLevel, + getReservation, getSessionUser, getStaff, + getVoucher, getStaffProfile, listAppointments, listBaristaQueue, @@ -110,7 +119,6 @@ import { publicMenu, publicShop, registerUser, - requestPasswordReset, resetPassword, revokeVoucher, platformStats, @@ -138,8 +146,10 @@ import { import { buildS3ObjectKey, callConfiguredAi, + deleteS3Object, providerCredentialStatus, publishSocial, + s3DeleteConfigStatus, uploadS3Object } from "@/server/integrations/external"; import { fail, ok } from "@/server/shared/api"; @@ -212,10 +222,23 @@ function canAccessShop(user: Awaited>, shopId?: s if (!shopId) return true; return Boolean(user?.roles?.some((role) => role.code === "superadmin" || - ((role.code === "admin" || role.code === "staff") && role.shop_id === shopId) + ((role.code === "admin" || role.code === "staff" || role.code === "marketing") && role.shop_id === shopId) )); } +function canAccessShopAs(user: Awaited>, shopId: string | null | undefined, roles: string[]) { + if (!shopId) return false; + return Boolean(user?.roles?.some((role) => role.code === "superadmin" || (roles.includes(String(role.code)) && role.shop_id === shopId))); +} + +function allowedShopIds(user: Awaited>) { + if (hasAnyRole(user, ["superadmin"])) return null; + return new Set((user?.roles ?? []) + .filter((role) => role.code === "admin" || role.code === "staff" || role.code === "marketing") + .map((role) => stringValue(role.shop_id)) + .filter((value): value is string => Boolean(value))); +} + async function requireRoles(roles: string[]) { const user = await currentUser(); if (!user) return { status: 401, message: "Authentication required" }; @@ -230,6 +253,44 @@ async function requireShopAccess(shopId?: string | null) { return canAccessShop(user, shopId) ? null : { status: 403, message: "Shop access required" }; } +async function requireShopAdminAccess(shopId?: string | null) { + if (!shopId) return { status: 400, message: "shopId is required" }; + const roleDenied = await requireRoles(["admin"]); + if (roleDenied) return roleDenied; + return requireShopAccess(shopId); +} + +async function requireShopRoleAccess(shopId: string | null | undefined, roles: string[]) { + if (!shopId) return { status: 400, message: "shopId is required" }; + const user = await currentUser(); + if (!user) return { status: 401, message: "Authentication required" }; + return canAccessShopAs(user, shopId, roles) ? null : { status: 403, message: "Shop role required" }; +} + +function recordShopId(record: unknown) { + if (!record || typeof record !== "object") return null; + const scoped = record as { shopId?: unknown; shop_id?: unknown }; + return stringValue(scoped.shopId) ?? stringValue(scoped.shop_id); +} + +async function requireRecordShopAccess(record: unknown, requestedShopId?: string | null) { + const shopId = recordShopId(record); + if (!shopId) return { status: 403, message: "Shop scope is required" }; + if (requestedShopId && requestedShopId !== shopId) return { status: 404, message: "Resource not found in shop" }; + return requireShopAccess(shopId); +} + +async function requireRecordShopRoleAccess(record: unknown, roles: string[], requestedShopId?: string | null) { + const shopId = recordShopId(record); + if (!shopId) return { status: 403, message: "Shop scope is required" }; + if (requestedShopId && requestedShopId !== shopId) return { status: 404, message: "Resource not found in shop" }; + return requireShopRoleAccess(shopId, roles); +} + +function requestShopId(body: Record, url: URL) { + return stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId")); +} + async function authorizeQueryScope(path: string[], url: URL) { const user = await currentUser(); if (!user) return { status: 401, message: "Authentication required" }; @@ -237,7 +298,7 @@ async function authorizeQueryScope(path: string[], url: URL) { if (path[0] === "shops" && path[1]) return requireShopAccess(path[1]); const queryShopId = stringValue(url.searchParams.get("shopId")); if (queryShopId) return requireShopAccess(queryShopId); - if (["products", "categories", "inventory", "orders", "staff", "members", "campaigns", "promotions", "files", "folders", "ai"].includes(path[0] ?? "") && hasAnyRole(user, ["customer"])) { + if (["products", "categories", "inventory", "orders", "pos", "staff", "members", "campaigns", "promotions", "vouchers", "reports", "kitchen", "files", "folders", "ai"].includes(path[0] ?? "") && hasAnyRole(user, ["customer"])) { return { status: 403, message: "Forbidden" }; } return null; @@ -250,14 +311,51 @@ async function authorizeBodyScope(path: string[], body: Record) if (denied) return denied; } if (path[0] === "shops" && path.length === 1) return requireRoles(["admin"]); - if (["staff", "leave-requests", "campaigns", "promotions", "vouchers", "files", "folders", "ai"].includes(path[0] ?? "")) { - return requireRoles(["admin", "staff"]); - } + if (path[0] === "marketing") return requireRoles(["admin", "marketing"]); + if (path[0] === "staff" && path[1] === "me") return requireRoles(["staff", "admin"]); + if (["staff", "files", "folders"].includes(path[0] ?? "")) return requireRoles(["admin"]); + if (path[0] === "leave-requests") return path[2] === "approve" || path[2] === "reject" ? requireRoles(["admin"]) : requireRoles(["admin", "staff"]); + if (["campaigns", "promotions", "vouchers"].includes(path[0] ?? "")) return requireRoles(["admin", "marketing"]); + if (path[0] === "ai") return requireRoles(["admin", "staff"]); return null; } function periodParam(value: string | null) { - return value === "week" || value === "month" || value === "30d" ? value : "today"; + return value === "week" || value === "7d" || value === "month" || value === "30d" ? value : "today"; +} + +function orderFilterParam(value: string | null) { + if (value === "all" || value === "today" || value === "week" || value === "7d" || value === "month" || value === "30d") { + return value; + } + return "today"; +} + +function unsupportedPaymentAdapter(method: unknown) { + const normalized = stringValue(method)?.toLowerCase().replace(/-/g, "_"); + if (!normalized) return false; + return !["cash", "customer_order", "kitchen_order", "room_fnb"].includes(normalized); +} + +function requiredShopId(value: unknown) { + const shopId = stringValue(value); + return shopId ? { shopId } : { error: fail("shopId is required", { status: 400 }) }; +} + +async function queryShopIdOrSuperadmin(url: URL) { + const shopId = stringValue(url.searchParams.get("shopId")); + if (shopId) return { shopId }; + const user = await currentUser(); + if (hasAnyRole(user, ["superadmin"])) return { shopId: null }; + return { error: fail("shopId is required", { status: 400 }) }; +} + +async function requireRequestShopRoleScope(body: Record, url: URL, roles: string[]) { + const shopId = requestShopId(body, url); + if (!shopId) return { error: fail("shopId is required", { status: 400 }) }; + const denied = await requireShopRoleAccess(shopId, roles); + if (denied) return { error: fail(denied.message, { status: denied.status }) }; + return { shopId }; } export async function GET(request: Request, context: RouteContext) { @@ -276,13 +374,18 @@ export async function GET(request: Request, context: RouteContext) { if (path[0] === "account") { if (path[1] === "me" || path[1] === "profile") return ok(await currentUser()); if (path[1] === "subscription" && path[2] === "plans") return ok(await listPlans()); - if (path[1] === "subscription") return ok({ plan: "growth", status: "active", usage: await platformStats() }); - return ok({ user: await currentUser(), linkedAccounts: [], twoFactorEnabled: false }); + if (path[1] === "subscription") return fail("Account subscription requires persisted billing state", { status: 501 }); + return ok({ user: await currentUser() }); } if (path[0] === "shops") { - if (path[1] === "stats") return ok(await getShopStatsService()); - if (path.length === 1) return ok(await listShopsService()); + if (path[1] === "stats") return ok(await getShopStatsService()); + if (path.length === 1) { + const shops = await listShopsService(); + const user = await currentUser(); + const shopIds = allowedShopIds(user); + return ok(shopIds ? shops.filter((shop) => shopIds.has(shop.id)) : shops); + } const shopId = path[1]; if (!shopId) return fail("Shop id is required", { status: 400 }); @@ -292,6 +395,14 @@ export async function GET(request: Request, context: RouteContext) { return shop ? ok(shop) : fail("Shop not found", { status: 404 }); } + if (path[2] === "products" && path[3] === "lookup") { + const term = stringValue(url.searchParams.get("barcode")) ?? stringValue(url.searchParams.get("sku")) ?? stringValue(url.searchParams.get("q")); + if (!term) return fail("barcode, sku or q is required", { status: 400 }); + const normalized = term.toLowerCase(); + const products = await listCatalogProductsByShop(shopId); + const product = products.find((item) => item.barcode?.toLowerCase() === normalized || item.sku?.toLowerCase() === normalized || item.name.toLowerCase() === normalized); + return product ? ok(product) : fail("Product not found", { status: 404 }); + } if (path[2] === "products") return ok(await listCatalogProductsByShop(shopId)); if (path[2] === "categories") return ok(await listCatalogCategoriesByShop(shopId)); if (path[2] === "inventory") return ok(await listInventoryItems(shopId)); @@ -302,7 +413,7 @@ export async function GET(request: Request, context: RouteContext) { shopId, page: numberValue(url.searchParams.get("page"), 1), pageSize: numberValue(url.searchParams.get("pageSize"), 40), - filter: stringValue(url.searchParams.get("filter")) === "all" ? "all" : "today" + filter: orderFilterParam(url.searchParams.get("filter")) }) ); } @@ -321,67 +432,111 @@ export async function GET(request: Request, context: RouteContext) { if (path[2] === "attendance") return ok(await getAttendance(null, shopId)); } - if (path[0] === "products") { - if (path.length === 1) { - return ok(await listCatalogProducts({ - shopId: stringValue(url.searchParams.get("shopId")), - includeInactive: boolValue(url.searchParams.get("includeInactive")) - })); - } + if (path[0] === "products") { + if (path.length === 1) { + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listCatalogProducts({ + shopId: scoped.shopId, + includeInactive: boolValue(url.searchParams.get("includeInactive")) + })); + } const product = await getCatalogProduct(path[1] ?? ""); + if (product) { + const productDenied = await requireRecordShopAccess(product, stringValue(url.searchParams.get("shopId"))); + if (productDenied) return fail(productDenied.message, { status: productDenied.status }); + } return product ? ok(product) : fail("Product not found", { status: 404 }); } if (path[0] === "categories") { if (path[1]) { const category = await getCatalogCategory(path[1]); + if (category) { + const categoryDenied = await requireRecordShopAccess(category, stringValue(url.searchParams.get("shopId"))); + if (categoryDenied) return fail(categoryDenied.message, { status: categoryDenied.status }); + } return category ? ok(category) : fail("Category not found", { status: 404 }); - } - return ok(await listCatalogCategories({ - shopId: stringValue(url.searchParams.get("shopId")), - includeInactive: boolValue(url.searchParams.get("includeInactive")) - })); - } + } + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listCatalogCategories({ + shopId: scoped.shopId, + includeInactive: boolValue(url.searchParams.get("includeInactive")) + })); + } if (path[0] === "inventory") { if (path[1] === "items" && path[2]) { const item = await getInventoryService(path[2]); + if (item) { + const itemDenied = await requireRecordShopAccess(item, stringValue(url.searchParams.get("shopId"))); + if (itemDenied) return fail(itemDenied.message, { status: itemDenied.status }); + } return item ? ok(item) : fail("Inventory item not found", { status: 404 }); } - if (path[1] === "transactions") return ok((await listInventoryWithTransactions(stringValue(url.searchParams.get("shopId")))).transactions); - if (path[1] === "low-stock") return ok((await listInventoryItems(stringValue(url.searchParams.get("shopId")) ?? "")).filter((item) => item.quantity <= item.reorderLevel)); - return ok(await listInventoryItems(stringValue(url.searchParams.get("shopId")) ?? "")); - } + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + if (path[1] === "transactions") return ok((await listInventoryWithTransactions(scoped.shopId)).transactions); + if (path[1] === "low-stock") return ok((await listInventoryItems(scoped.shopId ?? "")).filter((item) => item.quantity <= item.reorderLevel)); + return ok(await listInventoryItems(scoped.shopId ?? "")); + } if (path[0] === "tables" && path[1] === "by-token" && path[2]) { const table = await getTableFromToken(path[2]); return table ? ok(table) : fail("Table not found", { status: 404 }); } + if (path[0] === "tables" && path.length === 1) { + const shopId = stringValue(url.searchParams.get("shopId")); + if (!shopId) return fail("shopId is required", { status: 400 }); + return ok(await listTablesByShop(shopId)); + } + if (path[0] === "tables" && path[1]) { + const table = await getTable(path[1]); + if (table) { + const tableDenied = await requireRecordShopAccess(table, stringValue(url.searchParams.get("shopId"))); + if (tableDenied) return fail(tableDenied.message, { status: tableDenied.status }); + } + return table ? ok(table) : fail("Table not found", { status: 404 }); + } if (path[0] === "orders") { - if (path[1] === "active-by-table") return ok(await listActiveOrdersByTableService(stringValue(url.searchParams.get("shopId")))); + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + if (path[1] === "active-by-table") return ok(await listActiveOrdersByTableService(scoped.shopId)); if (path.length === 1) { return ok( await listOrdersService({ - shopId: stringValue(url.searchParams.get("shopId")), + shopId: scoped.shopId, page: numberValue(url.searchParams.get("page"), 1), pageSize: numberValue(url.searchParams.get("pageSize"), 40), - filter: stringValue(url.searchParams.get("filter")) === "today" ? "today" : "all" + filter: orderFilterParam(url.searchParams.get("filter")) }) ); } - const order = await getOrderService(path[1] ?? "", stringValue(url.searchParams.get("shopId"))); + if (path[2] === "returns") return fail("Order return history requires persisted return/exchange records", { status: 501 }); + const order = await getOrderService(path[1] ?? "", scoped.shopId); return order ? ok(order) : fail("Order not found", { status: 404 }); } if (path[0] === "pos" && path[1] === "dashboard") { - return ok(await getPosDashboardService(stringValue(url.searchParams.get("shopId")), periodParam(url.searchParams.get("period")))); + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await getPosDashboardService(scoped.shopId, periodParam(url.searchParams.get("period")))); } if (path[0] === "staff") { - if (path.length === 1) return ok(await listStaff(stringValue(url.searchParams.get("shopId")))); + if (path.length === 1) { + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listStaff(scoped.shopId)); + } if (path[1] === "roles") return ok(await listRoles(stringValue(url.searchParams.get("portal")))); - if (path[1] === "schedules") return ok(await listSchedules(stringValue(url.searchParams.get("shopId")))); + if (path[1] === "schedules") { + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listSchedules(scoped.shopId)); + } if (path[1] === "me") { const user = await currentUser(); const staff = await currentStaffProfile(); @@ -393,18 +548,32 @@ export async function GET(request: Request, context: RouteContext) { return ok(staff); } const staff = await getStaff(path[1] ?? ""); + if (staff) { + const staffDenied = await requireRecordShopAccess(staff, stringValue(url.searchParams.get("shopId"))); + if (staffDenied) return fail(staffDenied.message, { status: staffDenied.status }); + } return staff ? ok(staff) : fail("Staff not found", { status: 404 }); } if (path[0] === "kitchen" && path[1] === "tickets") { - return ok(await listKitchenTickets(stringValue(url.searchParams.get("shopId")), stringValue(url.searchParams.get("status")))); + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listKitchenTickets(scoped.shopId, stringValue(url.searchParams.get("status")))); } if (path[0] === "members") { - if (path.length === 1) return ok(await listMembers(stringValue(url.searchParams.get("search")), stringValue(url.searchParams.get("shopId")))); - if (path[2] === "progress") return ok(await getMemberProgress(path[1] ?? "")); - if (path[2] === "experience") return ok(await listMemberExperience(path[1] ?? "")); + if (path.length === 1) { + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listMembers(stringValue(url.searchParams.get("search")), scoped.shopId)); + } const member = await getMember(path[1] ?? ""); + if (member) { + const memberDenied = await requireRecordShopAccess(member, stringValue(url.searchParams.get("shopId"))); + if (memberDenied) return fail(memberDenied.message, { status: memberDenied.status }); + } + if (path[2] === "progress") return member ? ok(await getMemberProgress(path[1] ?? "")) : fail("Member not found", { status: 404 }); + if (path[2] === "experience") return member ? ok(await listMemberExperience(path[1] ?? "")) : fail("Member not found", { status: 404 }); return member ? ok(member) : fail("Member not found", { status: 404 }); } @@ -420,34 +589,65 @@ export async function GET(request: Request, context: RouteContext) { if (path[0] === "promotions" || path[0] === "campaigns") { if (path[1]) { const campaign = await getCampaign(path[1]); + if (campaign) { + const campaignDenied = await requireRecordShopAccess(campaign, stringValue(url.searchParams.get("shopId"))); + if (campaignDenied) return fail(campaignDenied.message, { status: campaignDenied.status }); + } return campaign ? ok(campaign) : fail("Campaign not found", { status: 404 }); } - return ok(await listCampaigns()); + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listCampaigns(scoped.shopId)); + } + if (path[0] === "vouchers" && (!path[1] || path[1] === "list")) { + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + return ok(await listVouchers(scoped.shopId)); } - if (path[0] === "vouchers" && !path[1]) return ok(await listVouchers(stringValue(url.searchParams.get("shopId")))); if (path[0] === "vouchers" && path[1] === "validate" && path[2]) return ok(await validateVoucher(path[2], stringValue(url.searchParams.get("shopId")))); if (path[0] === "reports") { - const shopId = stringValue(url.searchParams.get("shopId")); - if (path[1] === "top-products") return ok(await reportTopProducts(shopId)); - if (path[1] === "revenue" || path[1] === "revenue-analytics") return ok(await reportRevenue(shopId)); - if (path[1] === "eod") return ok({ closed: false, revenue: await reportRevenue(shopId), generatedAt: new Date().toISOString() }); - if (path[1] === "staff-performance") return ok(await listStaff(shopId)); + const scoped = await queryShopIdOrSuperadmin(url); + if ("error" in scoped) return scoped.error; + if (path[1] === "top-products") return ok(await reportTopProducts(scoped.shopId)); + if (path[1] === "revenue" || path[1] === "revenue-analytics") return ok(await reportRevenue(scoped.shopId)); + if (path[1] === "eod") return fail("EOD report requires persisted close-day ledger support", { status: 501 }); + if (path[1] === "staff-performance") return ok(await listStaff(scoped.shopId)); } if (path[0] === "files") { + const scoped = requiredShopId(url.searchParams.get("shopId")); + if ("error" in scoped) return scoped.error; if (path[1]) { - const file = await getFileRecord(path[1]); + const file = await getFileRecord(path[1], scoped.shopId) as ({ public_url?: string | null; access_url?: string | null } & Record) | null; + if (!file) return fail("File not found", { status: 404 }); + if (path[2] === "download-url") { + const publicUrl = stringValue(file.public_url) ?? stringValue(file.access_url); + return publicUrl ? ok({ url: publicUrl }) : fail("Private S3 signed download URLs are not implemented yet", { status: 501 }); + } + if (path[2] === "download") { + const publicUrl = stringValue(file.public_url) ?? stringValue(file.access_url); + return publicUrl ? Response.redirect(publicUrl, 302) : fail("Private S3 signed downloads are not implemented yet", { status: 501 }); + } return file ? ok(file) : fail("File not found", { status: 404 }); } - return ok(await listFiles(stringValue(url.searchParams.get("shopId")))); + return ok(await listFiles(scoped.shopId)); + } + if (path[0] === "folders") { + const scoped = requiredShopId(url.searchParams.get("shopId")); + if ("error" in scoped) return scoped.error; + if (path[1]) { + const folder = await getFolder(path[1], scoped.shopId); + return folder ? ok(folder) : fail("Folder not found", { status: 404 }); + } + return ok(await listFolders(scoped.shopId, stringValue(url.searchParams.get("parentId")))); } - if (path[0] === "folders") return ok(await listFolders(stringValue(url.searchParams.get("parentId")))); if (path[0] === "ai") { if (path[1] === "config") { - const shopId = stringValue(url.searchParams.get("shopId")); - return ok(shopId ? await getAiConfig(shopId) : null); + const scoped = requiredShopId(url.searchParams.get("shopId")); + if ("error" in scoped) return scoped.error; + return ok(await getAiConfig(scoped.shopId)); } if (path[1] === "providers") return ok(providerCredentialStatus()); } @@ -476,7 +676,7 @@ export async function GET(request: Request, context: RouteContext) { } } - if (path[0] === "devices") return ok([]); + if (path[0] === "devices") return fail("Device management requires persisted device registry support", { status: 501 }); if (path[0] === "integrations") return ok(providerCredentialStatus()); if (path[0] === "public" && path[1] === "shops" && path[2]) { if (path[3] === "menu") return ok(await publicMenu(path[2])); @@ -498,16 +698,20 @@ export async function POST(request: Request, context: RouteContext) { if (path[0] === "files" && path[1] === "upload") { const url = urlFromRequest(request); const form = await request.formData(); - const uploadScopeDenied = await requireShopAccess(stringValue(form.get("shopId")) ?? stringValue(url.searchParams.get("shopId"))); + const scoped = requiredShopId(stringValue(form.get("shopId")) ?? stringValue(url.searchParams.get("shopId"))); + if ("error" in scoped) return scoped.error; + const uploadScopeDenied = await requireShopRoleAccess(scoped.shopId, ["admin"]); if (uploadScopeDenied) return fail(uploadScopeDenied.message, { status: uploadScopeDenied.status }); const file = form.get("file"); if (!(file instanceof File)) return fail("file is required", { status: 400 }); + const folderId = stringValue(form.get("folderId")) ?? stringValue(url.searchParams.get("folderId")); + if (folderId && !(await getFolder(folderId, scoped.shopId))) return fail("Folder not found", { status: 404 }); const key = buildS3ObjectKey(file.name); const accessLevel = stringValue(form.get("accessLevel")) ?? stringValue(url.searchParams.get("accessLevel")) ?? "public"; const publicUrl = await uploadS3Object(key, file, accessLevel); return ok(await createFileRecord({ - shopId: stringValue(form.get("shopId")) ?? stringValue(url.searchParams.get("shopId")), - folderId: stringValue(form.get("folderId")) ?? stringValue(url.searchParams.get("folderId")), + shopId: scoped.shopId, + folderId, fileName: file.name, contentType: file.type, byteSize: file.size, @@ -517,12 +721,13 @@ export async function POST(request: Request, context: RouteContext) { })); } - const body = await readJson(request); - const scopeDenied = await authorizeBodyScope(path, body); + const body = await readJson(request); + const url = urlFromRequest(request); + const scopeDenied = await authorizeBodyScope(path, body); if (scopeDenied) return fail(scopeDenied.message, { status: scopeDenied.status }); if (path[0] === "auth" && path[1] === "login") { - const result = await loginUser(String(body.email ?? ""), String(body.password ?? "")); + const result = await loginUser(String(body.email ?? ""), String(body.password ?? ""), stringValue(body.role)); const jar = await cookies(); jar.set(sessionCookieName(), result.token, { httpOnly: true, @@ -542,14 +747,15 @@ export async function POST(request: Request, context: RouteContext) { } if (path[0] === "auth" && path[1] === "register") return ok(await registerUser(body), { status: 201 }); - if (path[0] === "auth" && (path[1] === "forgot-password" || path[1] === "forgot-password-new" || path[1] === "password-reset-request")) return ok(await requestPasswordReset(body)); + if (path[0] === "auth" && (path[1] === "forgot-password" || path[1] === "forgot-password-new" || path[1] === "password-reset-request")) { + return fail("Password reset delivery is not configured for MVP yet", { status: 501 }); + } if (path[0] === "auth" && (path[1] === "reset-password" || path[1] === "password-reset")) return ok(await resetPassword(body)); if (path[0] === "auth" && (path[1] === "verify-email" || path[1] === "email-verify")) return ok(await verifyEmail(body)); if (path[0] === "shops" && path.length === 1) { const activeDays = Array.isArray(body.activeDays) ? body.activeDays.map(String) : null; - return ok( - await createShopService({ + const shop = await createShopService({ name: String(body.name ?? ""), vertical: String(body.vertical ?? "retail"), slug: stringValue(body.slug), @@ -562,58 +768,97 @@ export async function POST(request: Request, context: RouteContext) { openTime: stringValue(body.openTime), closeTime: stringValue(body.closeTime), activeDays - }), - { status: 201 } - ); + }); + const user = await currentUser(); + if (user?.id && !hasAnyRole(user, ["superadmin"])) { + await assignUserShopRole(user.id, shop.id, "admin"); + } + return ok(shop, { status: 201 }); + } + if (path[0] === "shops" && path[1] && ["publish", "deactivate", "close"].includes(path[2] ?? "")) { + const shopDenied = await requireShopAdminAccess(path[1]); + if (shopDenied) return fail(shopDenied.message, { status: shopDenied.status }); + const statusId = path[2] === "publish" ? 2 : path[2] === "deactivate" ? 3 : 4; + return ok(await updateShopService(path[1], { statusId })); } - if (path[0] === "shops" && path[2] === "publish") return ok(await updateShopService(path[1] ?? "", { statusId: 2 })); - if (path[0] === "shops" && path[2] === "deactivate") return ok(await updateShopService(path[1] ?? "", { statusId: 3 })); - if (path[0] === "shops" && path[2] === "close") return ok(await updateShopService(path[1] ?? "", { statusId: 4 })); - if (path[0] === "products") return ok(await createCatalogProduct({ - shopId: String(body.shopId ?? ""), - name: String(body.name ?? ""), - price: numberValue(body.price), - vertical: stringValue(body.vertical), - categoryId: stringValue(body.categoryId), - description: stringValue(body.description), - sku: stringValue(body.sku), - barcode: stringValue(body.barcode), - initialQuantity: numberValue(body.initialQuantity) - }), { status: 201 }); + if (path[0] === "products" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + const categoryId = stringValue(body.categoryId); + if (categoryId) { + const category = await getCatalogCategory(categoryId); + if (!category) return fail("Category not found", { status: 404 }); + const categoryDenied = await requireRecordShopRoleAccess(category, ["admin"], scoped.shopId); + if (categoryDenied) return fail(categoryDenied.message, { status: categoryDenied.status }); + } + return ok(await createCatalogProduct({ + shopId: scoped.shopId, + name: String(body.name ?? ""), + price: numberValue(body.price), + vertical: stringValue(body.vertical), + categoryId, + description: stringValue(body.description), + sku: stringValue(body.sku), + barcode: stringValue(body.barcode), + initialQuantity: numberValue(body.initialQuantity) + }), { status: 201 }); + } - if (path[0] === "categories") return ok(await createCatalogCategory({ - shopId: String(body.shopId ?? ""), - name: String(body.name ?? ""), - description: stringValue(body.description), - displayOrder: numberValue(body.displayOrder) - }), { status: 201 }); + if (path[0] === "categories" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createCatalogCategory({ + shopId: scoped.shopId, + name: String(body.name ?? ""), + description: stringValue(body.description), + displayOrder: numberValue(body.displayOrder) + }), { status: 201 }); + } - if (path[0] === "inventory" && path[1] === "items") return ok(await createInventoryItemService({ - shopId: String(body.shopId ?? ""), - name: String(body.name ?? ""), - itemTypeId: numberValue(body.itemTypeId, 1), - unit: String(body.unit ?? "pcs"), - costPerUnit: numberValue(body.costPerUnit), - quantity: numberValue(body.quantity), - reorderLevel: numberValue(body.reorderLevel, 10), - supplierName: stringValue(body.supplierName) - }), { status: 201 }); - if (path[0] === "inventory" && path[1] === "stock-in") return ok(await stockIn(body as never)); - if (path[0] === "inventory" && path[1] === "stock-out") return ok(await stockOut(body as never)); - if (path[0] === "inventory" && path[1] === "adjust") return ok(await inventoryAdjust(body as never)); - if (path[0] === "inventory" && path[1] === "wastage") return ok(await stockOut({ ...body, notes: stringValue(body.reason) ?? "Wastage" } as never)); - if (path[0] === "inventory" && path[1] === "stocktake") { - const items = Array.isArray(body.items) ? body.items : []; - const adjustedItems = []; - const discrepancies = []; + if (path[0] === "inventory" && path[1] === "items" && path.length === 2) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createInventoryItemService({ + shopId: scoped.shopId, + name: String(body.name ?? ""), + itemTypeId: numberValue(body.itemTypeId, 1), + unit: String(body.unit ?? "pcs"), + costPerUnit: numberValue(body.costPerUnit), + quantity: numberValue(body.quantity), + reorderLevel: numberValue(body.reorderLevel, 10), + supplierName: stringValue(body.supplierName) + }), { status: 201 }); + } + if (path[0] === "inventory" && ["stock-in", "stock-out", "adjust", "wastage"].includes(path[1] ?? "")) { + const inventoryId = stringValue(body.inventoryId); + if (!inventoryId) return fail("inventoryId is required", { status: 400 }); + const existing = await getInventoryService(inventoryId); + if (!existing) return fail("Inventory item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + if ((path[1] === "stock-out" || path[1] === "wastage") && numberValue(body.quantity) > Number(existing.quantity ?? 0)) { + return fail("Insufficient inventory quantity", { status: 409 }); + } + if (path[1] === "stock-in") return ok(await stockIn(body as never)); + if (path[1] === "stock-out") return ok(await stockOut(body as never)); + if (path[1] === "adjust") return ok(await inventoryAdjust(body as never)); + return ok(await stockOut({ ...body, notes: stringValue(body.reason) ?? "Wastage" } as never)); + } + if (path[0] === "inventory" && path[1] === "stocktake") { + const items = Array.isArray(body.items) ? body.items : []; + const stocktakeShopId = requestShopId(body, url); + const adjustedItems = []; + const discrepancies = []; for (const rawItem of items) { const item = rawItem as Record; const inventoryId = stringValue(item.inventoryId) ?? stringValue(item.id); - if (!inventoryId) continue; - const current = await getInventoryService(inventoryId); - if (!current) throw new Error(`Inventory item not found: ${inventoryId}`); - const countedQuantity = numberValue(item.countedQuantity ?? item.quantity, Number(current.quantity ?? 0)); + if (!inventoryId) continue; + const current = await getInventoryService(inventoryId); + if (!current) throw new Error(`Inventory item not found: ${inventoryId}`); + const denied = await requireRecordShopRoleAccess(current, ["admin"], stringValue(item.shopId) ?? stringValue(item.shop_id) ?? stocktakeShopId); + if (denied) return fail(denied.message, { status: denied.status }); + const countedQuantity = numberValue(item.countedQuantity ?? item.quantity, Number(current.quantity ?? 0)); if (Number(current.quantity ?? 0) !== countedQuantity) { discrepancies.push({ inventoryId, @@ -632,26 +877,60 @@ export async function POST(request: Request, context: RouteContext) { return ok({ totalItemsCounted: items.length, discrepancies, adjustedItems }); } - if (path[0] === "tables" && path[2] === "generate-qr") return ok(await regenerateTableQrService(path[1] ?? "")); - if (path[0] === "tables") return ok(await createTableService({ - shopId: String(body.shopId ?? ""), - tableNumber: String(body.tableNumber ?? ""), - capacity: numberValue(body.capacity, 2), - zone: stringValue(body.zone), - hourlyRate: numberValue(body.hourlyRate) - }), { status: 201 }); - - if ((path[0] === "orders" && path.length === 1) || (path[0] === "pos" && path[1] === "orders")) { - return ok(await createPosOrder(body as Parameters[0]), { status: 201 }); + if (path[0] === "tables" && path[2] === "generate-qr") { + const existing = await getTable(path[1] ?? ""); + if (!existing) return fail("Table not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin", "staff"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await regenerateTableQrService(path[1] ?? "")); } - if (path[0] === "orders" && path[2] === "pay") return ok(await payOrderService(path[1] ?? "", { - shopId: stringValue(body.shopId), - paymentMethod: stringValue(body.paymentMethod), - amountTendered: body.amountTendered == null ? null : numberValue(body.amountTendered) - })); - if (path[0] === "orders" && path[2] === "cancel") return ok(await cancelOrderService(path[1] ?? "", stringValue(body.shopId), stringValue(body.reason))); + if (path[0] === "tables" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createTableService({ + shopId: scoped.shopId, + tableNumber: String(body.tableNumber ?? ""), + capacity: numberValue(body.capacity, 2), + zone: stringValue(body.zone), + hourlyRate: numberValue(body.hourlyRate) + }), { status: 201 }); + } - if (path[0] === "staff" && path.length === 1) return ok(await createStaff(body), { status: 201 }); + if ((path[0] === "orders" && path.length === 1) || (path[0] === "pos" && path[1] === "orders")) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "staff"]); + if ("error" in scoped) return scoped.error; + if (unsupportedPaymentAdapter(body.paymentMethod)) { + return fail("Payment adapter is not configured for this method", { status: 501 }); + } + return ok(await createPosOrder({ ...body, shopId: scoped.shopId } as Parameters[0]), { status: 201 }); + } + if (path[0] === "orders" && path[2] === "pay") { + const shopId = requestShopId(body, url); + if (!shopId) return fail("shopId is required", { status: 400 }); + const denied = await requireShopRoleAccess(shopId, ["admin", "staff"]); + if (denied) return fail(denied.message, { status: denied.status }); + if (unsupportedPaymentAdapter(body.paymentMethod)) { + return fail("Payment adapter is not configured for this method", { status: 501 }); + } + return ok(await payOrderService(path[1] ?? "", { + shopId, + paymentMethod: stringValue(body.paymentMethod), + amountTendered: body.amountTendered == null ? null : numberValue(body.amountTendered) + })); + } + if (path[0] === "orders" && path[2] === "cancel") { + const shopId = requestShopId(body, url); + if (!shopId) return fail("shopId is required", { status: 400 }); + const denied = await requireShopRoleAccess(shopId, ["admin", "staff"]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await cancelOrderService(path[1] ?? "", shopId, stringValue(body.reason))); + } + + if (path[0] === "staff" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createStaff({ ...body, shopId: scoped.shopId }), { status: 201 }); + } if (path[0] === "staff" && path[1] === "me") { const staff = await currentStaffProfile(); if (!staff?.id) return fail("Staff profile not found", { status: 404 }); @@ -659,46 +938,124 @@ export async function POST(request: Request, context: RouteContext) { if (path[2] === "attendance" && path[3] === "check-out") return ok(await checkOut(staff.id)); if (path[2] === "leave-requests") return ok(await createLeaveRequest({ ...body, staffId: staff.id, shopId: stringValue(staff.shop_id) }), { status: 201 }); } - if (path[0] === "leave-requests" && path[2] === "approve") return ok(await updateLeaveStatus(path[1] ?? "", "approved")); - if (path[0] === "leave-requests" && path[2] === "reject") return ok(await updateLeaveStatus(path[1] ?? "", "rejected")); - if (path[0] === "staff" && path[1] === "invite-with-account") return ok(await createStaff(body), { status: 201 }); - if (path[0] === "staff" && path[1] === "reset-password") { - const email = stringValue(body.email); - if (!email) return fail("email is required for staff password reset", { status: 400 }); - return ok(await requestPasswordReset({ email })); - } + if (path[0] === "leave-requests" && (path[2] === "approve" || path[2] === "reject")) { + const leave = await getLeaveRequest(path[1] ?? ""); + if (!leave) return fail("Leave request not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(leave, ["admin"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateLeaveStatus(path[1] ?? "", path[2] === "approve" ? "approved" : "rejected")); + } + if (path[0] === "staff" && path[1] === "invite-with-account") { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createStaff({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "staff" && path[1] === "reset-password") { + const email = stringValue(body.email); + if (!email) return fail("email is required for staff password reset", { status: 400 }); + return fail("Staff password reset delivery is not configured for MVP yet", { status: 501 }); + } - if (path[0] === "members" && path.length === 1) return ok(await createMember(body), { status: 201 }); - if (path[0] === "members" && path[2] === "experience") return ok(await addExperience(path[1] ?? "", numberValue(body.points), String(body.sourceId ?? "manual"), stringValue(body.referenceId))); - if (path[0] === "campaigns" && path[2] === "activate") return ok(await setCampaignStatus(path[1] ?? "", "active")); - if (path[0] === "campaigns" && path[2] === "pause") return ok(await setCampaignStatus(path[1] ?? "", "paused")); - if (path[0] === "campaigns") return ok(await createCampaign(body), { status: 201 }); - if (path[0] === "vouchers" && path[1] === "redeem") return ok(await redeemVoucher(body)); - if (path[0] === "vouchers" && path[2] === "revoke") return ok(await revokeVoucher(path[1] ?? "")); + if (path[0] === "members" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "marketing"]); + if ("error" in scoped) return scoped.error; + return ok(await createMember({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "members" && path[2] === "experience") { + const member = await getMember(path[1] ?? ""); + if (!member) return fail("Member not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(member, ["admin", "marketing"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await addExperience(path[1] ?? "", numberValue(body.points), String(body.sourceId ?? "manual"), stringValue(body.referenceId))); + } + if (path[0] === "campaigns" && (path[2] === "activate" || path[2] === "pause")) { + const campaign = await getCampaign(path[1] ?? ""); + if (!campaign) return fail("Campaign not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(campaign, ["admin", "marketing"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await setCampaignStatus(path[1] ?? "", path[2] === "activate" ? "active" : "paused")); + } + if (path[0] === "campaigns" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "marketing"]); + if ("error" in scoped) return scoped.error; + return ok(await createCampaign({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "vouchers" && path[1] === "redeem") return ok(await redeemVoucher(body)); + if (path[0] === "vouchers" && path[2] === "revoke") { + const voucher = await getVoucher(path[1] ?? ""); + if (!voucher) return fail("Voucher not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(voucher, ["admin", "marketing"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await revokeVoucher(path[1] ?? "", recordShopId(voucher))); + } - if (path[0] === "appointments") return ok(await createAppointment(body), { status: 201 }); - if (path[0] === "reservations") return ok(await createReservation(body), { status: 201 }); - if (path[0] === "kitchen" && path[1] === "tickets") return ok(await createKitchenTicket(body), { status: 201 }); - if (path[0] === "cafe" && path[1] === "barista-queue" && path[2]) { - const status = path[3] === "ready" ? "Ready" : path[3] === "delivered" ? "Delivered" : "InProgress"; - return ok(await updateBaristaQueue(path[2], status, stringValue(body.baristaName))); - } - if (path[0] === "folders") return ok(await createFolder(body), { status: 201 }); + if (path[0] === "appointments" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "staff"]); + if ("error" in scoped) return scoped.error; + return ok(await createAppointment({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "reservations" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "staff"]); + if ("error" in scoped) return scoped.error; + const tableId = stringValue(body.tableId); + if (tableId) { + const table = await getTable(tableId); + if (!table) return fail("Table not found", { status: 404 }); + const tableDenied = await requireRecordShopRoleAccess(table, ["admin", "staff"], scoped.shopId); + if (tableDenied) return fail(tableDenied.message, { status: tableDenied.status }); + } + return ok(await createReservation({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "kitchen" && path[1] === "tickets" && path.length === 2) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "staff"]); + if ("error" in scoped) return scoped.error; + const orderId = stringValue(body.orderId); + if (orderId && !(await getOrderService(orderId, scoped.shopId))) return fail("Order not found", { status: 404 }); + const tableId = stringValue(body.tableId); + if (tableId) { + const table = await getTable(tableId); + if (!table) return fail("Table not found", { status: 404 }); + const tableDenied = await requireRecordShopRoleAccess(table, ["admin", "staff"], scoped.shopId); + if (tableDenied) return fail(tableDenied.message, { status: tableDenied.status }); + } + return ok(await createKitchenTicket({ ...body, shopId: scoped.shopId }), { status: 201 }); + } + if (path[0] === "cafe" && path[1] === "barista-queue" && path[2]) { + const queueItem = await getBaristaQueueItem(path[2]); + if (!queueItem) return fail("Barista queue item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(queueItem, ["admin", "staff"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + const status = path[3] === "ready" ? "Ready" : path[3] === "delivered" ? "Delivered" : "InProgress"; + return ok(await updateBaristaQueue(path[2], status, stringValue(body.baristaName))); + } + if (path[0] === "folders" && path.length === 1) { + const scoped = await requireRequestShopRoleScope(body, url, ["admin"]); + if ("error" in scoped) return scoped.error; + return ok(await createFolder({ ...body, shopId: scoped.shopId }), { status: 201 }); + } - if (path[0] === "ai" && path[1] === "chat") { - const provider = stringValue(body.provider) ?? process.env.AI_DEFAULT_PROVIDER ?? "openai"; - const message = Array.isArray(body.messages) - ? String((body.messages[body.messages.length - 1] as { content?: unknown } | undefined)?.content ?? "") - : String(body.message ?? ""); - const response = await callConfiguredAi(provider, message); - await saveAiMessage({ shopId: stringValue(body.shopId), role: "user", content: message }); - await saveAiMessage({ shopId: stringValue(body.shopId), role: "assistant", content: JSON.stringify(response) }); - return ok({ content: response, toolsUsed: [] }); - } + if (path[0] === "ai" && path[1] === "chat") { + const scoped = await requireRequestShopRoleScope(body, url, ["admin", "staff"]); + if ("error" in scoped) return scoped.error; + const provider = stringValue(body.provider) ?? process.env.AI_DEFAULT_PROVIDER ?? "openai"; + const message = Array.isArray(body.messages) + ? String((body.messages[body.messages.length - 1] as { content?: unknown } | undefined)?.content ?? "") + : String(body.message ?? ""); + const response = await callConfiguredAi(provider, message); + await saveAiMessage({ shopId: scoped.shopId, role: "user", content: message }); + await saveAiMessage({ shopId: scoped.shopId, role: "assistant", content: JSON.stringify(response) }); + return ok({ content: response, toolsUsed: [] }); + } - if (path[0] === "marketing" && path[1] === "publish" && path[2]) { - return ok(await publishSocial(path[2], body)); - } + if (path[0] === "marketing" && path[1] === "publish" && path[2]) { + const roleDenied = await requireRoles(["admin", "marketing"]); + if (roleDenied) return fail(roleDenied.message, { status: roleDenied.status }); + const shopId = requestShopId(body, url); + if (!shopId) return fail("shopId is required", { status: 400 }); + const denied = await requireShopRoleAccess(shopId, ["admin", "marketing"]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await publishSocial(path[2], body)); + } if (path[0] === "reports" && path[1] === "close-day") { return fail("Close-day requires persisted EOD ledger support before it can be marked closed", { status: 501 }); @@ -715,29 +1072,139 @@ export async function PUT(request: Request, context: RouteContext) { const path = (await context.params).path ?? []; const accessDenied = await authorizeBff(path, request.method); if (accessDenied) return fail(accessDenied.message, { status: accessDenied.status }); + const url = urlFromRequest(request); const body = await readJson(request); const scopeDenied = await authorizeBodyScope(path, body); if (scopeDenied) return fail(scopeDenied.message, { status: scopeDenied.status }); - if (path[0] === "account" && (path[1] === "profile" || path[1] === "me")) return ok({ ...(await currentUser()), ...body }); - if (path[0] === "shops" && path[2] === "settings") return ok(await updateShopSettingsService(path[1] ?? "", body)); - if (path[0] === "shops" && path.length === 2) return ok(await updateShopService(path[1] ?? "", body)); - if (path[0] === "products") return ok(await updateCatalogProduct(path[1] ?? "", body)); - if (path[0] === "categories") return ok(await updateCatalogCategory(path[1] ?? "", body)); - if (path[0] === "tables" && path[2] === "status") return ok(await updateTableStatusService(path[1] ?? "", numberValue(body.statusId, 1))); - if (path[0] === "tables") return ok(await updateTableService(path[1] ?? "", body)); - if (path[0] === "inventory" && path[1] === "items" && path[2]) return ok(await updateInventoryItemService(path[2], body)); - if (path[0] === "inventory" && path[2] === "adjust") return ok(await inventoryAdjust({ inventoryId: path[1] ?? "", quantity: numberValue(body.quantity), notes: stringValue(body.notes) })); - if (path[0] === "inventory") return ok(await updateInventoryItemService(path[1] ?? "", body)); - if (path[0] === "orders" && path[2] === "cancel") return ok(await cancelOrderService(path[1] ?? "", stringValue(body.shopId), stringValue(body.reason))); - if (path[0] === "staff" && path[1] && path[1] !== "me") return ok(await updateStaff(path[1], body)); - if (path[0] === "members" && path[1]) return ok(await updateMember(path[1], body)); - if ((path[0] === "campaigns" || path[0] === "promotions") && path[1]) return ok(await updateCampaign(path[1], body)); - if (path[0] === "files" && path[1]) return ok(await updateFileRecord(path[1], body)); - if (path[0] === "appointments") return ok(await updateAppointmentStatus(path[1] ?? "", String(body.action ?? body.status ?? "confirmed"))); - if (path[0] === "reservations") return ok(await updateReservationStatus(path[1] ?? "", String(body.status ?? "confirmed"))); - if (path[0] === "kitchen" && path[1] === "tickets") return ok(await updateKitchenTicket(path[2] ?? "", String(body.status ?? "InProgress"))); - if (path[0] === "ai" && path[1] === "config") return ok(await saveAiConfig(body)); + if (path[0] === "account" && (path[1] === "profile" || path[1] === "me")) { + return fail("Account profile update requires persisted user profile support", { status: 501 }); + } + if (path[0] === "shops" && path[2] === "settings") { + const denied = await requireShopAdminAccess(path[1]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateShopSettingsService(path[1] ?? "", body)); + } + if (path[0] === "shops" && path.length === 2) { + const denied = await requireShopAdminAccess(path[1]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateShopService(path[1] ?? "", body)); + } + if (path[0] === "products") { + const existing = await getCatalogProduct(path[1] ?? ""); + if (!existing) return fail("Product not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + const categoryId = stringValue(body.categoryId); + if (categoryId) { + const category = await getCatalogCategory(categoryId); + if (!category) return fail("Category not found", { status: 404 }); + const categoryDenied = await requireRecordShopRoleAccess(category, ["admin"], recordShopId(existing)); + if (categoryDenied) return fail(categoryDenied.message, { status: categoryDenied.status }); + } + return ok(await updateCatalogProduct(path[1] ?? "", body)); + } + if (path[0] === "categories") { + const existing = await getCatalogCategory(path[1] ?? ""); + if (!existing) return fail("Category not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateCatalogCategory(path[1] ?? "", body)); + } + if (path[0] === "tables") { + const existing = await getTable(path[1] ?? ""); + if (!existing) return fail("Table not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + if (path[2] === "status") return ok(await updateTableStatusService(path[1] ?? "", numberValue(body.statusId, 1))); + return ok(await updateTableService(path[1] ?? "", body)); + } + if (path[0] === "inventory" && path[1] === "items" && path[2]) { + const existing = await getInventoryService(path[2]); + if (!existing) return fail("Inventory item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateInventoryItemService(path[2], body)); + } + if (path[0] === "inventory" && path[2] === "adjust") { + const existing = await getInventoryService(path[1] ?? ""); + if (!existing) return fail("Inventory item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await inventoryAdjust({ inventoryId: path[1] ?? "", quantity: numberValue(body.quantity), notes: stringValue(body.notes) })); + } + if (path[0] === "inventory") { + const existing = await getInventoryService(path[1] ?? ""); + if (!existing) return fail("Inventory item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateInventoryItemService(path[1] ?? "", body)); + } + if (path[0] === "orders" && path[2] === "cancel") { + const shopId = requestShopId(body, url); + if (!shopId) return fail("shopId is required", { status: 400 }); + const denied = await requireShopRoleAccess(shopId, ["admin", "staff"]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await cancelOrderService(path[1] ?? "", shopId, stringValue(body.reason))); + } + if (path[0] === "staff" && path[1] && path[1] !== "me") { + const existing = await getStaff(path[1]); + if (!existing) return fail("Staff not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateStaff(path[1], body)); + } + if (path[0] === "members" && path[1]) { + const existing = await getMember(path[1]); + if (!existing) return fail("Member not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin", "marketing"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateMember(path[1], body)); + } + if ((path[0] === "campaigns" || path[0] === "promotions") && path[1]) { + const existing = await getCampaign(path[1]); + if (!existing) return fail("Campaign not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin", "marketing"], stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateCampaign(path[1], body)); + } + if (path[0] === "files" && path[1]) { + const scoped = requiredShopId(stringValue(body.shopId) ?? stringValue(body.shop_id) ?? stringValue(url.searchParams.get("shopId"))); + if ("error" in scoped) return scoped.error; + const fileScopeDenied = await requireShopRoleAccess(scoped.shopId, ["admin"]); + if (fileScopeDenied) return fail(fileScopeDenied.message, { status: fileScopeDenied.status }); + return ok(await updateFileRecord(path[1], scoped.shopId, body)); + } + if (path[0] === "appointments") { + const appointment = await getAppointment(path[1] ?? ""); + if (!appointment) return fail("Appointment not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(appointment, ["admin", "staff"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + const action = path[2] === "cancel" ? "cancel" : String(body.action ?? body.status ?? "confirmed"); + return ok(await updateAppointmentStatus(path[1] ?? "", action)); + } + if (path[0] === "reservations") { + const reservation = await getReservation(path[1] ?? ""); + if (!reservation) return fail("Reservation not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(reservation, ["admin", "staff"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + const status = path[2] === "cancel" ? "cancelled" : String(body.status ?? "confirmed"); + return ok(await updateReservationStatus(path[1] ?? "", status)); + } + if (path[0] === "kitchen" && path[1] === "tickets") { + const ticket = await getKitchenTicket(path[2] ?? ""); + if (!ticket) return fail("Kitchen ticket not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(ticket, ["admin", "staff"], requestShopId(body, url)); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateKitchenTicket(path[2] ?? "", String(body.status ?? "InProgress"))); + } + if (path[0] === "ai" && path[1] === "config") { + const shopId = requestShopId(body, url); + if (!shopId) return fail("shopId is required", { status: 400 }); + const denied = await requireShopRoleAccess(shopId, ["admin"]); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await saveAiConfig({ ...body, shopId })); + } if (path[0] === "superadmin" && path[1] === "feature-flags" && path[2]) return ok(await updateFeatureFlag(path[2], Boolean(body.enabled))); if (path[0] === "superadmin" && path[1] === "system" && path[2] === "flags" && path[3]) return ok(await updateFeatureFlag(path[3], Boolean(body.enabled))); return fail("BFF route not found", { status: 404 }); @@ -746,6 +1213,10 @@ export async function PUT(request: Request, context: RouteContext) { } } +export async function PATCH(request: Request, context: RouteContext) { + return PUT(request, context); +} + export async function DELETE(request: Request, context: RouteContext) { try { const path = (await context.params).path ?? []; @@ -753,16 +1224,94 @@ export async function DELETE(request: Request, context: RouteContext) { if (accessDenied) return fail(accessDenied.message, { status: accessDenied.status }); const deleteDenied = await requireRoles(["admin"]); if (deleteDenied) return fail(deleteDenied.message, { status: deleteDenied.status }); - if (path[0] === "products") return ok(await deleteCatalogProduct(path[1] ?? "")); - if (path[0] === "categories") return ok(await deleteCatalogCategory(path[1] ?? "")); - if (path[0] === "tables") return ok(await removeTableService(path[1] ?? "")); - if (path[0] === "inventory" && path[1] === "items") return ok(await deleteInventoryItemService(path[2] ?? "")); - if (path[0] === "staff") return ok(await deleteStaff(path[1] ?? "")); - if (path[0] === "members") return ok(await deleteMember(path[1] ?? "")); - if (path[0] === "campaigns") return ok(await setCampaignStatus(path[1] ?? "", "disabled")); - if (path[0] === "folders") return ok(await deleteFolder(path[1] ?? "")); - if (path[0] === "files") return ok(await deleteFileRecord(path[1] ?? "")); - return fail("BFF route not found", { status: 404 }); + const url = urlFromRequest(request); + if (path[0] === "products") { + const existing = await getCatalogProduct(path[1] ?? ""); + if (!existing) return fail("Product not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await deleteCatalogProduct(path[1] ?? "")); + } + if (path[0] === "categories") { + const existing = await getCatalogCategory(path[1] ?? ""); + if (!existing) return fail("Category not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await deleteCatalogCategory(path[1] ?? "")); + } + if (path[0] === "tables") { + const existing = await getTable(path[1] ?? ""); + if (!existing) return fail("Table not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await removeTableService(path[1] ?? "")); + } + if (path[0] === "inventory" && path[1] === "items") { + const existing = await getInventoryService(path[2] ?? ""); + if (!existing) return fail("Inventory item not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await deleteInventoryItemService(path[2] ?? "")); + } + if (path[0] === "staff") { + const existing = await getStaff(path[1] ?? ""); + if (!existing) return fail("Staff not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await deleteStaff(path[1] ?? "")); + } + if (path[0] === "members") { + const existing = await getMember(path[1] ?? ""); + if (!existing) return fail("Member not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin", "marketing"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await deleteMember(path[1] ?? "")); + } + if (path[0] === "campaigns") { + const existing = await getCampaign(path[1] ?? ""); + if (!existing) return fail("Campaign not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(existing, ["admin", "marketing"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await setCampaignStatus(path[1] ?? "", "disabled")); + } + if (path[0] === "folders") { + const scoped = requiredShopId(url.searchParams.get("shopId")); + if ("error" in scoped) return scoped.error; + const folderScopeDenied = await requireShopRoleAccess(scoped.shopId, ["admin"]); + if (folderScopeDenied) return fail(folderScopeDenied.message, { status: folderScopeDenied.status }); + return ok(await deleteFolder(path[1] ?? "", scoped.shopId)); + } + if (path[0] === "files") { + const scoped = requiredShopId(url.searchParams.get("shopId")); + if ("error" in scoped) return scoped.error; + const fileScopeDenied = await requireShopRoleAccess(scoped.shopId, ["admin"]); + if (fileScopeDenied) return fail(fileScopeDenied.message, { status: fileScopeDenied.status }); + const file = await getFileRecord(path[1] ?? "", scoped.shopId) as ({ object_key?: string | null; provider?: string | null } & Record) | null; + if (!file) return fail("File not found", { status: 404 }); + if (file.object_key && String(file.provider ?? "s3").toLowerCase() === "s3") { + const s3Config = s3DeleteConfigStatus(); + if (!s3Config.configured) { + throw new Error(`S3 cleanup configuration is incomplete: missing ${s3Config.missing.join(", ")}`); + } + await deleteS3Object(file.object_key); + } + return ok(await deleteFileRecord(path[1] ?? "", scoped.shopId)); + } + if (path[0] === "appointments" && path[2] === "cancel") { + const appointment = await getAppointment(path[1] ?? ""); + if (!appointment) return fail("Appointment not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(appointment, ["admin", "staff"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateAppointmentStatus(path[1] ?? "", "cancel")); + } + if (path[0] === "reservations" && path[2] === "cancel") { + const reservation = await getReservation(path[1] ?? ""); + if (!reservation) return fail("Reservation not found", { status: 404 }); + const denied = await requireRecordShopRoleAccess(reservation, ["admin", "staff"], stringValue(url.searchParams.get("shopId"))); + if (denied) return fail(denied.message, { status: denied.status }); + return ok(await updateReservationStatus(path[1] ?? "", "cancelled")); + } + return fail("BFF route not found", { status: 404 }); } catch (error) { return fail(error instanceof Error ? error.message : "BFF request failed", { status: 400 }); } diff --git a/microservices/apps/tpos-mvp-next/src/app/globals.css b/microservices/apps/tpos-mvp-next/src/app/globals.css index 970fdd8f..61eb2697 100644 --- a/microservices/apps/tpos-mvp-next/src/app/globals.css +++ b/microservices/apps/tpos-mvp-next/src/app/globals.css @@ -2404,6 +2404,13 @@ button:disabled { cursor: pointer; } +.pos-bottom-nav__tab span { + max-width: 100%; + line-height: 1.15; + overflow-wrap: anywhere; + text-align: center; +} + .pos-bottom-nav__tab--active, .pos-bottom-nav__tab:hover { border-color: rgba(255, 92, 0, 0.42); @@ -2620,6 +2627,11 @@ button:disabled { color: #ffffff; } +.pos-payment-method-btn:disabled { + cursor: not-allowed; + opacity: 0.46; +} + .pos-payment-quick-amounts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -2679,13 +2691,21 @@ button:disabled { display: none; } - .pos-clone .pos-content-area { - grid-template-columns: minmax(0, 1fr); - } + .pos-clone .pos-content-area { + grid-template-columns: minmax(0, 1fr); + overflow: auto; + } - .pos-cart-panel { - display: none; - } + .pos-cart-panel { + display: flex; + width: 100%; + min-width: 0; + height: auto; + min-height: 560px; + border-top: 1px solid #202024; + border-left: 0; + overflow: visible; + } } @media (max-width: 760px) { @@ -2714,15 +2734,16 @@ button:disabled { flex-direction: column; } - .pos-clone .pos-content-area { - flex: 1; - display: block; - overflow: hidden; - } + .pos-clone .pos-content-area { + flex: 1; + display: block; + overflow-y: auto; + } - .pos-product-panel { - height: calc(100vh - 50px - 64px); - } + .pos-product-panel { + height: auto; + min-height: calc(100vh - 50px - 64px); + } .pos-clone .pos-bottom-nav { order: 2; @@ -3450,6 +3471,7 @@ textarea { } .pos-bottom-nav__tab { + position: relative; min-height: auto; gap: 3px; margin: 0 6px; @@ -3467,6 +3489,17 @@ textarea { color: #ff5c00; } +.pos-bottom-nav__tab--active::before { + content: ""; + position: absolute; + top: 10px; + bottom: 10px; + left: 0; + width: 3px; + border-radius: 999px; + background: #ff5c00; +} + .pos-clone .pos-history { grid-column: 1; grid-row: 1; @@ -4391,7 +4424,8 @@ textarea { gap: 6px; } -.workflow-action-panel input { +.workflow-action-panel input, +.workflow-action-panel select { min-height: 44px; border: 1px solid #2a2a2e; border-radius: 10px; @@ -4400,6 +4434,37 @@ textarea { padding: 0 12px; } +.workflow-action-panel--methods { + align-items: stretch; + grid-template-columns: minmax(0, 1fr) minmax(280px, 440px); +} + +.workflow-payment-method-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.workflow-payment-method { + min-height: 74px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 8px; + border: 1px solid #2a2a2e; + border-radius: 12px; + background: #0a0a0b; + color: #ffffff; + font-weight: 800; + text-decoration: none; +} + +.workflow-payment-method:disabled { + cursor: not-allowed; + opacity: 0.44; +} + /* Customer QR menu follows the original compact white mobile menu */ .customer-menu { min-height: 100vh; @@ -4802,13 +4867,21 @@ textarea { grid-template-columns: 1fr; } - .pos-clone .pos-content-area { - grid-template-columns: minmax(0, 1fr); - } + .pos-clone .pos-content-area { + grid-template-columns: minmax(0, 1fr); + overflow: auto; + } - .pos-cart-panel { - display: none; - } + .pos-cart-panel { + display: flex; + width: 100%; + min-width: 0; + height: auto; + min-height: 560px; + border-top: 1px solid #202024; + border-left: 0; + overflow: visible; + } .workflow-action-panel { grid-template-columns: 1fr; @@ -4839,15 +4912,16 @@ textarea { display: flex; } - .pos-clone .pos-bottom-nav { - width: 64px; + .pos-clone .pos-bottom-nav { + width: 64px; height: auto; order: 2; } - .pos-product-panel { - height: calc(100vh - 48px); - } + .pos-product-panel { + height: auto; + min-height: calc(100vh - 48px); + } .pos-product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -4925,12 +4999,12 @@ textarea { .landing-hero { position: relative; - min-height: 64vh; + min-height: 46vh; display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 64px 24px 48px; + padding: 52px 24px 30px; text-align: center; overflow: hidden; } @@ -5013,13 +5087,29 @@ textarea { color: #ff5c00; } +.home-hero__brand { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + color: #ffffff; + font-size: 28px; + font-weight: 800; +} + +.home-hero__brand svg { + color: #ff5c00; +} + .home-hero__badge { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 8px; - margin-bottom: 32px; + margin-bottom: 22px; padding: 8px 20px; border: 1px solid rgba(255, 92, 0, 0.25); border-radius: 999px; @@ -5033,9 +5123,9 @@ textarea { .home-hero__title { position: relative; z-index: 1; - max-width: 800px; - margin: 0 0 24px; - font-size: 48px; + max-width: 780px; + margin: 0 0 20px; + font-size: 44px; font-weight: 800; line-height: 1.08; letter-spacing: 0; @@ -5045,8 +5135,8 @@ textarea { .home-hero__subtitle { position: relative; z-index: 1; - max-width: 640px; - margin: 0 auto 40px; + max-width: 700px; + margin: 0 auto 30px; color: #adadb0; font-size: 16px; line-height: 1.7; @@ -5059,7 +5149,7 @@ textarea { justify-content: center; flex-wrap: wrap; gap: 16px; - margin-bottom: 48px; + margin-bottom: 30px; } .home-hero__btn { @@ -5111,9 +5201,33 @@ textarea { } .home-verticals { - max-width: 900px; + max-width: 1080px; margin: 0 auto; - padding: 48px 24px 64px; + padding: 14px 24px 44px; +} + +.home-verticals__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.home-verticals__head span { + color: #ffffff; + font-size: 18px; + font-weight: 800; +} + +.home-verticals__head a { + display: inline-flex; + align-items: center; + gap: 6px; + color: #ff8a4c; + font-size: 14px; + font-weight: 700; + text-decoration: none; } .home-verticals__grid { @@ -5123,13 +5237,13 @@ textarea { } .home-vertical-card { - min-height: 128px; + min-height: 150px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; - padding: 24px 16px; + padding: 22px 14px; border: 1px solid #1f1f23; border-radius: 14px; background: #111113; @@ -5144,12 +5258,48 @@ textarea { color: #ff5c00; } +.home-vertical-card span { + color: #ffffff; + font-size: 14px; +} + +.home-vertical-card p { + margin: 0; + color: #8b8b90; + font-size: 12px; + font-weight: 500; + line-height: 1.4; +} + .home-vertical-card:hover { border-color: #ff5c00; background: rgba(255, 92, 0, 0.15); color: #ffffff; } +.home-portal-strip { + max-width: 1080px; + min-height: 86px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin: 0 auto; + padding: 0 24px 48px; +} + +.home-portal-strip > div { + display: inline-flex; + align-items: center; + gap: 10px; + color: #ffffff; + font-weight: 700; +} + +.home-portal-strip svg { + color: #ff5c00; +} + .tpos-section, .project-intro, .landing-cta { @@ -5436,7 +5586,7 @@ textarea { } .home-verticals__grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(2, minmax(0, 1fr)); } .tpos-feature-grid, @@ -5464,7 +5614,8 @@ textarea { } .home-hero__actions, - .landing-cta div { + .landing-cta div, + .home-portal-strip { width: 100%; flex-direction: column; } @@ -5475,7 +5626,17 @@ textarea { .home-verticals__grid, .login-portal__grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: 1fr; + } + + .home-verticals__head { + align-items: flex-start; + flex-direction: column; + } + + .home-portal-strip { + align-items: stretch; + padding: 0 16px 36px; } } @@ -5747,11 +5908,11 @@ textarea { /* Final POS responsive guard: keep History/Dashboard out from under the right rail. */ @media (max-width: 760px) { - .pos-clone .pos-page-content { - display: grid; - grid-template-columns: minmax(0, 1fr) 64px; - height: calc(100vh - 48px); - } + .pos-clone .pos-page-content { + display: grid; + grid-template-columns: minmax(0, 1fr) 64px; + height: calc(100vh - 48px); + } .pos-clone .pos-history, .pos-clone .pos-dashboard, @@ -5778,6 +5939,15 @@ textarea { color: #f5f5f7; } +.admin-mobile-bar { + display: none; +} + +.admin-sidebar-overlay, +.admin-sidebar__close { + display: none; +} + .admin-sidebar { width: 260px; min-width: 260px; @@ -5802,6 +5972,10 @@ textarea { text-decoration: none; } +.admin-sidebar__logo-text { + flex: 1; +} + .admin-sidebar__logo-icon { width: 40px; height: 40px; @@ -5974,6 +6148,66 @@ textarea { color: #22c55e; } +.admin-reference-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.admin-reference-row { + min-height: 64px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 14px; + padding: 12px 14px; + border: 1px solid #242429; + border-radius: 8px; + background: #151518; + color: inherit; + text-decoration: none; +} + +.admin-reference-row:hover { + border-color: rgba(255, 92, 0, 0.55); + background: #1a1a1e; +} + +.admin-reference-row__main { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +} + +.admin-reference-row__main strong { + overflow: hidden; + color: #ffffff; + font-size: 14px; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-reference-row__main span { + overflow: hidden; + color: #8b8b90; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-reference-row > b { + color: #f5f5f7; + font-size: 13px; + font-weight: 650; + white-space: nowrap; +} + +.admin-reference-row > svg { + color: #77777d; +} + .admin-main { flex: 1; display: flex; @@ -6790,6 +7024,170 @@ textarea { font-size: 13px; } +.admin-inline-note { + min-height: 38px; + display: flex; + align-items: center; + gap: 8px; + border: 1px solid rgba(59, 130, 246, 0.18); + border-radius: 10px; + background: rgba(59, 130, 246, 0.08); + color: #adadb0; + padding: 9px 12px; + font-size: 12px; + font-weight: 600; +} + +.admin-inline-note svg { + flex-shrink: 0; + color: #60a5fa; +} + +.admin-table-wrap { + overflow-x: auto; +} + +.admin-data-table { + width: 100%; + min-width: 760px; + border-collapse: collapse; +} + +.admin-data-table th { + padding: 10px 12px; + border-bottom: 1px solid #2a2a2e; + color: #8b8b90; + font-size: 11px; + font-weight: 800; + letter-spacing: 0; + text-align: left; + text-transform: uppercase; +} + +.admin-data-table td { + padding: 11px 12px; + border-bottom: 1px solid #242428; + color: #d7d7db; + font-size: 13px; + vertical-align: middle; +} + +.admin-data-table td > b, +.admin-data-table td > strong { + display: block; + color: #ffffff; + font-size: 13px; +} + +.admin-data-table td > span { + display: block; + margin-top: 3px; + color: #8b8b90; + font-size: 11px; +} + +.admin-data-table .is-right { + text-align: right; +} + +.admin-finance-grid { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(300px, 0.55fr); + gap: 20px; +} + +.admin-finance-grid--bottom { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.admin-finance-chart { + min-height: 340px; +} + +.admin-finance-bars { + height: 250px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(44px, 1fr)); + gap: 10px; + align-items: end; + padding-top: 8px; +} + +.admin-finance-bar { + min-width: 0; + display: grid; + gap: 7px; + align-items: end; + text-align: center; +} + +.admin-finance-bar b { + overflow: hidden; + color: #ff7a2f; + font-size: 10px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-finance-bar > div { + height: 190px; + display: flex; + align-items: end; + border-radius: 9px; + background: #111114; + overflow: hidden; +} + +.admin-finance-bar span { + width: 100%; + display: block; + border-radius: 9px 9px 0 0; + background: linear-gradient(180deg, #ff7a2f 0%, #ff5c00 100%); +} + +.admin-finance-bar small { + color: #8b8b90; + font-size: 11px; +} + +.admin-compact-list { + display: grid; + gap: 8px; +} + +.admin-compact-row { + min-height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border-radius: 10px; + background: #202024; + padding: 10px 12px; +} + +.admin-compact-row > div { + min-width: 0; + display: grid; + gap: 2px; +} + +.admin-compact-row b, +.admin-compact-row strong { + overflow: hidden; + color: #ffffff; + font-size: 13px; + font-weight: 750; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-compact-row span { + color: #8b8b90; + font-size: 12px; +} + .spinner-small, .spin { animation: spin 0.9s linear infinite; @@ -6854,3 +7252,150 @@ textarea { align-items: flex-start; } } + +@media (max-width: 960px) { + .admin-layout { + min-height: 100dvh; + padding-top: 56px; + } + + .admin-mobile-bar { + position: fixed; + inset: 0 0 auto; + z-index: 75; + min-height: 56px; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 14px; + border-bottom: 1px solid #242429; + background: #161619; + } + + .admin-mobile-bar__button { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 1px solid #2d2d33; + border-radius: 8px; + background: #1c1c20; + color: #f5f5f7; + } + + .admin-mobile-bar div { + display: flex; + min-width: 0; + flex-direction: column; + } + + .admin-mobile-bar b { + color: #ffffff; + font-size: 14px; + } + + .admin-mobile-bar span { + overflow: hidden; + color: #8b8b90; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .admin-sidebar { + position: fixed; + z-index: 90; + inset: 0 auto 0 0; + transform: translateX(-100%); + transition: transform 0.2s ease; + } + + .admin-sidebar--open { + transform: translateX(0); + } + + .admin-sidebar-overlay { + position: fixed; + z-index: 85; + inset: 0; + display: block; + border: 0; + background: rgba(0, 0, 0, 0.55); + } + + .admin-sidebar__close { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid #2d2d33; + border-radius: 8px; + background: #1c1c20; + color: #f5f5f7; + } + + .admin-main { + width: 100%; + } + + .admin-topbar { + min-height: 0; + padding: 14px; + } + + .admin-topbar__right { + flex-wrap: wrap; + } + + .admin-search { + flex: 1 1 220px; + } + + .admin-finance-grid, + .admin-finance-grid--bottom { + grid-template-columns: 1fr; + } + + .admin-content { + padding: 14px; + } +} + +@media (max-width: 640px) { + .admin-reference-row { + grid-template-columns: minmax(0, 1fr); + align-items: start; + } + + .admin-reference-row > b { + white-space: normal; + } + + .admin-topbar__title { + font-size: 22px; + } +} + +@media (min-width: 761px) { + .pos-clone .pos-main { + display: grid; + grid-template-columns: 156px minmax(0, 1fr); + } + + .pos-clone .pos-sidebar { + position: static; + width: auto; + min-width: 0; + display: flex; + flex-direction: column; + gap: 10px; + padding: 14px 10px; + border-right: 1px solid #202024; + background: rgba(17, 17, 20, 0.96); + } + + .pos-clone .pos-page-content { + display: grid; + grid-template-columns: minmax(0, 1fr) 82px; + } +} diff --git a/microservices/apps/tpos-mvp-next/src/app/marketing/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/marketing/[...path]/page.tsx index 2c39829f..a6d6e108 100644 --- a/microservices/apps/tpos-mvp-next/src/app/marketing/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/marketing/[...path]/page.tsx @@ -1,5 +1,6 @@ import { notFound } from "next/navigation"; import { TposPortal, buildPortalPayload } from "@/components/TposPortal"; +import { portalShopId, requirePortalRole } from "@/server/auth/portal"; import { providerCredentialStatus } from "@/server/integrations/external"; import { listCampaigns, listMembers, reportRevenue } from "@/server/services/parity"; import { portalNav } from "@/components/tpos-config"; @@ -8,9 +9,11 @@ export const dynamic = "force-dynamic"; export default async function MarketingCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { const path = (await params).path ?? []; + const user = await requirePortalRole(["admin", "marketing"], `/marketing/${path.join("/")}`); + const shopId = portalShopId(user, "marketing") ?? portalShopId(user, "admin"); const section = path.join("/") || "marketing"; if (!isKnownMarketingSection(section)) notFound(); - const items = await loadItems(section); + const items = await loadItems(section, shopId); const status = providerCredentialStatus(); return ( ({ title: String(member.display_name ?? "Khách hàng"), meta: String(member.phone ?? ""), value: `Level ${member.current_level}` })); } if (section === "analytics") { - const rows = await reportRevenue(); + const rows = await reportRevenue(shopId); return rows.map((row) => ({ title: String(row.day), meta: `${row.order_count} đơn`, value: `${row.revenue} VND` })); } if (section === "content") { - const campaigns = await listCampaigns(); + const campaigns = await listCampaigns(shopId); return [ { title: "Lịch nội dung", meta: "Bài viết theo kênh", value: `${campaigns.length} chiến dịch` }, { title: "AI caption", meta: "Sinh nội dung qua provider đã cấu hình", value: "AI" }, @@ -58,7 +61,7 @@ async function loadItems(section: string) { ]; } if (section === "marketing") { - const campaigns = await listCampaigns(); + const campaigns = await listCampaigns(shopId); return campaigns.map((campaign) => ({ title: String(campaign.name), meta: String(campaign.description ?? "Campaign"), value: String(campaign.status) })); } notFound(); diff --git a/microservices/apps/tpos-mvp-next/src/app/marketing/page.tsx b/microservices/apps/tpos-mvp-next/src/app/marketing/page.tsx index 58ba4923..7632f294 100644 --- a/microservices/apps/tpos-mvp-next/src/app/marketing/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/marketing/page.tsx @@ -1,11 +1,14 @@ import { TposPortal, buildPortalPayload } from "@/components/TposPortal"; +import { portalShopId, requirePortalRole } from "@/server/auth/portal"; import { listCampaigns } from "@/server/services/parity"; import { providerCredentialStatus } from "@/server/integrations/external"; export const dynamic = "force-dynamic"; export default async function MarketingPage() { - const campaigns = await listCampaigns(); + const user = await requirePortalRole(["admin", "marketing"], "/marketing"); + const shopId = portalShopId(user, "marketing") ?? portalShopId(user, "admin"); + const campaigns = await listCampaigns(shopId); const status = providerCredentialStatus(); return ( ; + export default async function PosVerticalPage({ - params + params, + searchParams }: { params: Promise<{ shopId: string; vertical: string; workflow?: string[] }>; + searchParams?: Promise; }) { const { shopId, vertical, workflow } = await params; - return renderPosExperience(shopId, vertical, workflow); + const query = await searchParams; + const pathTab = posTabFromPath(workflow?.[0]); + const resolvedWorkflow = pathTab ? undefined : verticalFamilyWorkflow(workflow, query); + return renderPosExperience(shopId, vertical, resolvedWorkflow, pathTab ?? posTabFromQuery(query?.tab)); } diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/[...path]/page.tsx index 582ca152..def0e2e1 100644 --- a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/[...path]/page.tsx @@ -1,4 +1,4 @@ -import { firstQueryValue, renderPosExperience } from "../../../pos-experience"; +import { appendWorkflowContext, dialogWorkflow, firstQueryValue, renderPosExperience } from "../../../pos-experience"; export const dynamic = "force-dynamic"; @@ -13,27 +13,5 @@ export default async function PosDialogAlias({ }) { const { shopId, path } = await params; const query = await searchParams; - return renderPosExperience(shopId, firstQueryValue(query?.vertical), dialogWorkflow(path)); -} - -function dialogWorkflow(path: string[]) { - const [head, ...rest] = path.length ? path : ["order-edit"]; - const map: Record = { - order: "order-edit", - "order-edit": "order-edit", - note: "order-edit", - discount: "discount", - customer: "customer-select", - "customer-select": "customer-select", - table: "table-transfer", - "table-transfer": "table-transfer", - "split-bill": "split-bill", - cancel: "void-refund", - "order-cancel": "void-refund", - "price-check": "product-search", - "stock-in": "stock-check", - "stock-out": "stock-check", - "stock-transfer": "stock-check" - }; - return [map[head] ?? head, ...rest]; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), appendWorkflowContext(dialogWorkflow(path), query)); } diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/page.tsx new file mode 100644 index 00000000..1a866825 --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/dialog/page.tsx @@ -0,0 +1,17 @@ +import { appendWorkflowContext, dialogWorkflow, firstQueryValue, renderPosExperience } from "../../pos-experience"; + +export const dynamic = "force-dynamic"; + +type SearchParams = Record; + +export default async function PosDialogIndex({ + params, + searchParams +}: { + params: Promise<{ shopId: string }>; + searchParams?: Promise; +}) { + const { shopId } = await params; + const query = await searchParams; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), appendWorkflowContext(dialogWorkflow([]), query)); +} diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/[...path]/page.tsx index 97814adf..36488e96 100644 --- a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/[...path]/page.tsx @@ -1,4 +1,4 @@ -import { firstQueryValue, renderPosExperience } from "../../../pos-experience"; +import { appendWorkflowContext, firstQueryValue, operationWorkflow, renderPosExperience } from "../../../pos-experience"; export const dynamic = "force-dynamic"; @@ -13,29 +13,5 @@ export default async function PosOperationsAlias({ }) { const { shopId, path } = await params; const query = await searchParams; - return renderPosExperience(shopId, firstQueryValue(query?.vertical), operationWorkflow(path)); -} - -function operationWorkflow(path: string[]) { - const [head, ...rest] = path.length ? path : ["shift"]; - const map: Record = { - drawer: "cash-drawer", - "cash-drawer": "cash-drawer", - shift: "shift", - pending: "pending-orders", - "pending-orders": "pending-orders", - quick: "quick-sale", - "quick-sale": "quick-sale", - split: "split-bill", - "split-bill": "split-bill", - refund: "void-refund", - "void-refund": "void-refund", - "clock-in-out": "shift", - "stock-in": "stock-check", - "stock-out": "stock-check", - "stock-transfer": "stock-check", - "price-check": "product-search", - "order-cancel": "void-refund" - }; - return [map[head] ?? head, ...rest]; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), appendWorkflowContext(operationWorkflow(path), query)); } diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/page.tsx new file mode 100644 index 00000000..64faa0d6 --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/operations/page.tsx @@ -0,0 +1,17 @@ +import { appendWorkflowContext, firstQueryValue, operationWorkflow, renderPosExperience } from "../../pos-experience"; + +export const dynamic = "force-dynamic"; + +type SearchParams = Record; + +export default async function PosOperationsIndex({ + params, + searchParams +}: { + params: Promise<{ shopId: string }>; + searchParams?: Promise; +}) { + const { shopId } = await params; + const query = await searchParams; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), appendWorkflowContext(operationWorkflow([]), query)); +} diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/[...path]/page.tsx index a652a7a5..a90f1b68 100644 --- a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/[...path]/page.tsx @@ -1,4 +1,4 @@ -import { firstQueryValue, renderPosExperience } from "../../../pos-experience"; +import { firstQueryValue, paymentWorkflow, renderPosExperience } from "../../../pos-experience"; export const dynamic = "force-dynamic"; @@ -13,31 +13,5 @@ export default async function PosPaymentAlias({ }) { const { shopId, path } = await params; const query = await searchParams; - return renderPosExperience(shopId, firstQueryValue(query?.vertical), paymentWorkflow(path)); -} - -function paymentWorkflow(path: string[]) { - const [head, ...rest] = path.length ? path : ["method-select"]; - const map: Record = { - cash: "cash-payment", - "cash-payment": "cash-payment", - card: "card-payment", - "card-payment": "card-payment", - qr: "qr-payment", - "qr-payment": "qr-payment", - transfer: "transfer-payment", - "transfer-payment": "transfer-payment", - "gift-card": "gift-card-payment", - "gift-card-payment": "gift-card-payment", - "bank-transfer": "transfer-payment", - partial: "partial-payment", - "partial-payment": "partial-payment", - pending: "payment-pending", - "payment-pending": "payment-pending", - success: "payment-success", - "payment-success": "payment-success", - receipt: "payment-success", - tip: "partial-payment" - }; - return [map[head] ?? head, ...rest]; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), paymentWorkflow(path, firstQueryValue(query?.orderId))); } diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/page.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/page.tsx new file mode 100644 index 00000000..6b9544c8 --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/pos/[shopId]/payment/page.tsx @@ -0,0 +1,17 @@ +import { firstQueryValue, paymentWorkflow, renderPosExperience } from "../../pos-experience"; + +export const dynamic = "force-dynamic"; + +type SearchParams = Record; + +export default async function PosPaymentIndex({ + params, + searchParams +}: { + params: Promise<{ shopId: string }>; + searchParams?: Promise; +}) { + const { shopId } = await params; + const query = await searchParams; + return renderPosExperience(shopId, firstQueryValue(query?.vertical), paymentWorkflow([], firstQueryValue(query?.orderId))); +} diff --git a/microservices/apps/tpos-mvp-next/src/app/pos/pos-experience.tsx b/microservices/apps/tpos-mvp-next/src/app/pos/pos-experience.tsx index 773ce9eb..c7149edb 100644 --- a/microservices/apps/tpos-mvp-next/src/app/pos/pos-experience.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/pos/pos-experience.tsx @@ -1,38 +1,57 @@ import { notFound } from "next/navigation"; import { TposPosExperience } from "@/components/TposPosExperience"; +import { requirePortalRole } from "@/server/auth/portal"; import { getShopService } from "@/server/services/shop"; import { listCatalogCategoriesByShop, listCatalogProductsByShop } from "@/server/services/catalog"; import { listTablesByShop } from "@/server/services/fnb"; -import { getPosDashboardService, listOrdersService } from "@/server/services/order"; +import { listInventoryItems } from "@/server/services/inventory"; +import { getOrderService, getPosDashboardService, listOrdersService } from "@/server/services/order"; import { listBaristaQueue, listKitchenTickets } from "@/server/services/parity"; import type { VerticalKind } from "@/components/tpos-config"; -export async function renderPosExperience(shopId: string, vertical: string | null | undefined, workflow?: string[]) { +type InitialPosTab = "sale" | "history" | "dashboard" | "settings"; +type SearchParamRecord = Record; + +export async function renderPosExperience( + shopId: string, + vertical: string | null | undefined, + workflow?: string[], + initialTab: InitialPosTab = "sale" +) { if (!isUuid(shopId)) notFound(); const shop = await getShopService(shopId); if (!shop) notFound(); + await requirePortalRole(["admin", "staff"], `/pos/${shopId}/${vertical ?? shop.vertical}${workflow?.length ? `/${workflow.join("/")}` : ""}`, shopId); const normalizedVertical = normalizeVertical(vertical ?? shop.vertical); if (!normalizedVertical) notFound(); - const [products, categories, tables, orders, dashboard, kitchenTickets, baristaQueue] = await Promise.all([ + const contextOrderId = workflow?.[1] ?? null; + const [products, categories, tables, inventory, orders, dashboard, kitchenTickets, baristaQueue, paymentContextOrder] = await Promise.all([ listCatalogProductsByShop(shopId), listCatalogCategoriesByShop(shopId), listTablesByShop(shopId), - listOrdersService({ shopId, page: 1, pageSize: 24, filter: "all" }), + listInventoryItems(shopId), + listOrdersService({ shopId, page: 1, pageSize: 80, filter: "all" }), getPosDashboardService(shopId, "today"), listKitchenTickets(shopId), - listBaristaQueue(shopId) + listBaristaQueue(shopId), + contextOrderId ? getOrderService(contextOrderId, shopId).catch(() => null) : Promise.resolve(null) ]); + const orderItems = paymentContextOrder && !orders.items.some((order) => order.id === paymentContextOrder.id) + ? [paymentContextOrder, ...orders.items] + : orders.items; return ( } kitchenTickets={kitchenTickets} baristaQueue={baristaQueue} @@ -49,6 +68,108 @@ export function firstQueryValue(value?: string | string[]) { return Array.isArray(value) ? value[0] : value; } +export function posTabFromQuery(value?: string | string[] | null): InitialPosTab { + const tab = firstQueryValue(value ?? undefined); + return tab === "history" || tab === "dashboard" || tab === "settings" ? tab : "sale"; +} + +export function posTabFromPath(value?: string | null): InitialPosTab | null { + return value === "history" || value === "dashboard" || value === "settings" ? value : null; +} + +export function contextIdFromQuery(query?: SearchParamRecord) { + return firstQueryValue(query?.orderId) ?? firstQueryValue(query?.tableId) ?? firstQueryValue(query?.roomId); +} + +export function appendWorkflowContext(workflow: string[], query?: SearchParamRecord) { + const contextId = contextIdFromQuery(query); + return contextId && workflow.length === 1 ? [...workflow, contextId] : workflow; +} + +export function verticalFamilyWorkflow(path: string[] | undefined, query?: SearchParamRecord) { + if (!path?.length) return undefined; + const [family, head = "", ...rest] = path; + const familyPath = head ? [head, ...rest] : []; + if (family === "payment") return appendWorkflowContext(paymentWorkflow(familyPath, firstQueryValue(query?.orderId)), query); + if (family === "dialog") return appendWorkflowContext(dialogWorkflow(familyPath), query); + if (family === "operations") return appendWorkflowContext(operationWorkflow(familyPath), query); + return path; +} + +export function paymentWorkflow(path: string[], orderId?: string | null) { + const [head, ...rest] = path.length ? path : ["method-select"]; + const map: Record = { + cash: "cash-payment", + "cash-payment": "cash-payment", + card: "card-payment", + "card-payment": "card-payment", + qr: "qr-payment", + "qr-payment": "qr-payment", + transfer: "transfer-payment", + "transfer-payment": "transfer-payment", + "gift-card": "gift-card-payment", + "gift-card-payment": "gift-card-payment", + "bank-transfer": "transfer-payment", + partial: "partial-payment", + "partial-payment": "partial-payment", + pending: "payment-pending", + "payment-pending": "payment-pending", + success: "payment-success", + "payment-success": "payment-success", + receipt: "receipt", + tip: "tip" + }; + const workflow = [map[head] ?? head, ...rest]; + return orderId && workflow.length === 1 ? [...workflow, orderId] : workflow; +} + +export function dialogWorkflow(path: string[]) { + const [head, ...rest] = path.length ? path : ["order-edit"]; + const map: Record = { + edit: "order-edit", + "order-edit": "order-edit", + discount: "discount", + customer: "customer-select", + "customer-select": "customer-select", + table: "table-transfer", + "table-transfer": "table-transfer", + split: "split-bill", + "split-bill": "split-bill", + refund: "void-refund", + void: "void-refund", + "void-refund": "void-refund", + "price-check": "product-search", + "stock-in": "stock-in", + "stock-out": "stock-out", + "stock-transfer": "stock-transfer" + }; + return [map[head] ?? head, ...rest]; +} + +export function operationWorkflow(path: string[]) { + const [head, ...rest] = path.length ? path : ["shift"]; + const map: Record = { + drawer: "cash-drawer", + "cash-drawer": "cash-drawer", + shift: "shift", + pending: "pending-orders", + "pending-orders": "pending-orders", + quick: "quick-sale", + "quick-sale": "quick-sale", + split: "split-bill", + "split-bill": "split-bill", + refund: "void-refund", + "void-refund": "void-refund", + "clock-in-out": "shift", + "stock-in": "stock-check", + "stock-out": "stock-check", + "stock-transfer": "stock-check", + "price-check": "product-search", + "order-cancel": "void-refund" + }; + return [map[head] ?? head, ...rest]; +} + function isUuid(value: string) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); } diff --git a/microservices/apps/tpos-mvp-next/src/app/register/page.tsx b/microservices/apps/tpos-mvp-next/src/app/register/page.tsx index 41d86c5f..d5ee4f0d 100644 --- a/microservices/apps/tpos-mvp-next/src/app/register/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/register/page.tsx @@ -1,5 +1,5 @@ import { TposAuthBoundary } from "@/components/TposAuthBoundary"; export default function RegisterPage() { - return ; + return ; } diff --git a/microservices/apps/tpos-mvp-next/src/app/staff/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/staff/[...path]/page.tsx index 7e5ad846..2912255c 100644 --- a/microservices/apps/tpos-mvp-next/src/app/staff/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/staff/[...path]/page.tsx @@ -1,19 +1,23 @@ import { notFound, redirect } from "next/navigation"; import { TposPortal, buildPortalPayload } from "@/components/TposPortal"; +import { portalShopId, requirePortalRole } from "@/server/auth/portal"; import { getDashboardStats } from "@/server/db/queries"; import { listTablesByShop } from "@/server/services/fnb"; import { listOrdersService } from "@/server/services/order"; import { getShopService } from "@/server/services/shop"; -import { getAttendance, listKitchenTickets, listLeaveRequests, listNotifications, listSchedules, listStaff } from "@/server/services/parity"; +import { getAttendance, getStaffProfile, listKitchenTickets, listLeaveRequests, listNotifications, listSchedules, listStaff } from "@/server/services/parity"; import { portalNav } from "@/components/tpos-config"; export const dynamic = "force-dynamic"; export default async function StaffCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { const path = (await params).path ?? []; + const user = await requirePortalRole(["staff"], `/staff/${path.join("/")}`); const section = path.join("/") || "dashboard"; if (!isKnownStaffSection(section)) notFound(); - const shop = await getShopService(); + const shop = await getShopService(portalShopId(user, "staff")); + const staffProfile = await getStaffProfile(user.id); + if (!staffAllowedSections(String(staffProfile?.role ?? "staff")).has(section)) notFound(); if (section === "pos" && shop) { redirect(`/pos/${shop.id}/${normalizeVertical(shop.vertical)}`); } @@ -27,14 +31,15 @@ export default async function StaffCatchAllPage({ params }: { params: Promise<{ shop: shop ? { id: shop.id, name: shop.name, vertical: shop.vertical, status: shop.status } : null, stats, title: staffTitle(section), - metrics: [ - { label: "Ca hôm nay", value: "08:00-17:00", tone: "green" }, - { label: "Phiếu bếp/quầy", value: items.length, tone: "orange" }, - { label: "Trạng thái", value: "Sẵn sàng", tone: "blue" } - ], - items - })} - /> + metrics: [ + { label: "Ca hôm nay", value: "08:00-17:00", tone: "green" }, + { label: "Phiếu bếp/quầy", value: items.length, tone: "orange" }, + { label: "Trạng thái", value: "Sẵn sàng", tone: "blue" } + ], + items, + nav: staffNavForRole(String(staffProfile?.role ?? "staff")) + })} + /> ); } @@ -109,6 +114,41 @@ function isKnownStaffSection(section: string) { return sections.has(section); } +function sectionFromStaffHref(href: string) { + return href.replace(/^\/staff\/?/, "") || "dashboard"; +} + +function staffAllowedSections(role: string) { + const normalized = role.toLowerCase(); + const sections = new Set(["dashboard", "overview", "attendance", "schedule", "leave", "notifications"]); + if (normalized.includes("manager") || normalized.includes("lead") || normalized.includes("admin")) { + for (const [, , href] of portalNav.staff) sections.add(sectionFromStaffHref(href)); + return sections; + } + if (normalized.includes("kitchen") || normalized.includes("bếp") || normalized.includes("chef")) { + sections.add("kitchen"); + } + if (normalized.includes("waiter") || normalized.includes("phục vụ") || normalized.includes("server")) { + sections.add("pos"); + sections.add("tables"); + sections.add("kitchen"); + } + if (normalized.includes("cashier") || normalized.includes("thu ngân")) { + sections.add("pos"); + sections.add("tables"); + sections.add("payroll"); + } + if (sections.size === 6) sections.add("pos"); + return sections; +} + +function staffNavForRole(role: string) { + const allowed = staffAllowedSections(role); + return portalNav.staff + .filter(([, , href]) => allowed.has(sectionFromStaffHref(href))) + .map(([label, , href, Icon]) => ({ label, href, Icon })); +} + function money(value: number) { return new Intl.NumberFormat("vi-VN", { style: "currency", currency: "VND", maximumFractionDigits: 0 }).format(value); } diff --git a/microservices/apps/tpos-mvp-next/src/app/superadmin/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/superadmin/[...path]/page.tsx index 992a3513..4b3ad186 100644 --- a/microservices/apps/tpos-mvp-next/src/app/superadmin/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/superadmin/[...path]/page.tsx @@ -1,5 +1,6 @@ import { notFound } from "next/navigation"; import { TposPortal, buildPortalPayload } from "@/components/TposPortal"; +import { requirePortalRole } from "@/server/auth/portal"; import { auditLogs, listFeatureFlags, listPlans, listRoles, listUsers, platformStats, systemHealth } from "@/server/services/parity"; import { listShopsService } from "@/server/services/shop"; import { portalNav } from "@/components/tpos-config"; @@ -8,6 +9,7 @@ export const dynamic = "force-dynamic"; export default async function SuperAdminCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { const path = (await params).path ?? []; + await requirePortalRole(["superadmin"], `/superadmin/${path.join("/")}`); const section = path.join("/") || "dashboard"; if (!isKnownSuperAdminSection(section)) notFound(); const stats = await platformStats(); diff --git a/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx b/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx index aa50fd36..f1f6e330 100644 --- a/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx @@ -139,13 +139,13 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro setMessage("OTP đăng nhập khách hàng chưa được cấu hình trong MVP"); return; } - startTransition(async () => { - const response = await fetch("/api/bff/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }) - }); - const payload = (await response.json()) as { success: boolean; error?: string; data?: { roles?: Array<{ portal: string }>; defaultShopId?: string } }; + startTransition(async () => { + const response = await fetch("/api/bff/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, role: selected.role }) + }); + const payload = (await response.json()) as { success: boolean; error?: string; data?: { roles?: Array<{ code?: string; portal: string }>; defaultShopId?: string } }; if (!response.ok || !payload.success) { setMessage(payload.error ?? "Không thể đăng nhập"); return; @@ -155,7 +155,8 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro if (returnUrl.startsWith("/") && !returnUrl.startsWith("//")) router.push(returnUrl); return; } - const portal = payload.data?.roles?.[0]?.portal ?? "admin"; + const requestedPortal = selected.role === "branch" ? "admin" : selected.role; + const portal = payload.data?.roles?.find((item) => item.code === requestedPortal || item.portal === requestedPortal)?.portal ?? payload.data?.roles?.[0]?.portal ?? "admin"; router.push(portal === "staff" ? "/staff/dashboard" : portal === "superadmin" ? "/superadmin/dashboard" : portal === "customer" ? "/" : "/admin"); router.refresh(); }); @@ -321,14 +322,14 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro function flowCopy(mode: string) { const map: Record = { - register: { - title: "Đăng ký dùng thử", - description: "Tạo tài khoản merchant/customer MVP, chọn vai trò và bắt đầu onboarding.", - badge: "REGISTER", - formTitle: "Tạo tài khoản", - formText: "Dữ liệu ghi vào MVP DB auth, role và session sẽ dùng chung BFF.", - submit: "Tạo tài khoản", - success: "Đã tạo tài khoản. Có thể quay lại đăng nhập." + register: { + title: "Đăng ký tài khoản khách hàng", + description: "Tạo tài khoản loyalty để tích điểm, nhận voucher và theo dõi lịch sử mua hàng.", + badge: "REGISTER", + formTitle: "Tạo tài khoản", + formText: "Dữ liệu ghi vào MVP DB auth và dùng chung cho QR menu, ví điểm và voucher.", + submit: "Tạo tài khoản", + success: "Đã tạo tài khoản. Có thể quay lại đăng nhập." }, "forgot-password": { title: "Khôi phục mật khẩu", @@ -385,7 +386,7 @@ function AuthNav() { aPOS
Tính năng - Bảng giá + Bảng giá Đăng nhập Dùng thử miễn phí
diff --git a/microservices/apps/tpos-mvp-next/src/components/TposPortal.tsx b/microservices/apps/tpos-mvp-next/src/components/TposPortal.tsx index 83e08e7c..0123ac4b 100644 --- a/microservices/apps/tpos-mvp-next/src/components/TposPortal.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/TposPortal.tsx @@ -9,10 +9,12 @@ import { ShieldCheck, Store } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { portalNav, shopSections, type PortalKind, type VerticalKind } from "./tpos-config"; type Metric = { label: string; value: string | number; tone?: "orange" | "green" | "blue" | "red" }; type ListItem = { id?: string; title: string; meta?: string; value?: string; href?: string }; +type PortalNavItem = { label: string; href: string; Icon: LucideIcon }; export type PortalPayload = { title: string; @@ -22,6 +24,7 @@ export type PortalPayload = { primary?: ListItem[]; secondary?: ListItem[]; status?: Array<{ label: string; value: string; tone?: Metric["tone"] }>; + nav?: PortalNavItem[]; }; function labelFromPath(kind: PortalKind, segments: string[]) { @@ -40,7 +43,9 @@ export function TposPortal({ }) { const shopVertical = payload.shop && ((payload.shop.vertical ?? "cafe") as VerticalKind) in shopSections ? (payload.shop.vertical as VerticalKind) : "cafe"; const isShopAdmin = kind === "admin" && path[0] === "shop" && Boolean(payload.shop); - const nav = isShopAdmin && payload.shop + const nav = payload.nav + ? payload.nav.map(({ label, href, Icon }) => [label, href, href, Icon] as const) + : isShopAdmin && payload.shop ? shopSections[shopVertical].map(([label, slug, Icon]) => [ label, slug, @@ -50,6 +55,7 @@ export function TposPortal({ : portalNav[kind]; const active = labelFromPath(kind, path); const currentHref = `/${kind}${path.length ? `/${path.join("/")}` : ""}`; + const primaryAction = primaryActionFor(kind, path, payload.shop); const isActiveHref = (href: string) => { if (isShopAdmin && href.startsWith("/pos/")) return false; if (href === `/${kind}`) return path.length === 0 || path[0] === "dashboard"; @@ -92,10 +98,12 @@ export function TposPortal({ ) : null} - + {primaryAction ? ( + + + {primaryAction.label} + + ) : null} @@ -151,8 +159,8 @@ export function TposPortal({
- LINKS -

Route parity

+ LIÊN KẾT +

Truy cập nhanh

@@ -261,14 +269,44 @@ function defaultSecondary(kind: PortalKind, shop?: PortalPayload["shop"]): ListI { title: "Feature flags", href: "/superadmin/system/flags" }, { title: "Audit log", href: "/superadmin/system/audit" } ]; + if (kind === "marketing") return [ + { title: "Social hub", href: "/marketing" }, + { title: "Content studio", href: "/marketing/content" }, + { title: "Analytics", href: "/marketing/analytics" } + ]; + if (kind === "staff") return [ + { title: "Ca làm", href: "/staff/dashboard" }, + { title: "Bếp", href: "/staff/kitchen" }, + { title: "Điểm danh", href: "/staff/attendance" } + ]; const vertical = shop?.vertical ?? "cafe"; - return [ + return shop ? [ { title: "Customer menu", href: shop ? `/menu/${shop.id}` : "/" }, { title: "POS terminal", href: shop ? `/pos/${shop.id}/${vertical}` : "/pos" }, { title: "Settings", href: shop ? `/admin/shop/${shop.id}/settings` : "/settings" } + ] : [ + { title: "Cửa hàng", href: "/admin/stores" }, + { title: "Báo cáo", href: "/admin/reports" }, + { title: "Cài đặt", href: "/admin/settings" } ]; } +function primaryActionFor(kind: PortalKind, path: string[], shop?: PortalPayload["shop"]): { label: string; href: string } | null { + if (kind === "admin" && shop) { + const section = path[2] ?? "overview"; + if (section === "staff") return { label: "Thêm nhân sự", href: `/admin/shop/${shop.id}/staff` }; + if (section === "inventory") return { label: "Nhập kho", href: `/admin/shop/${shop.id}/inventory` }; + if (section === "tables" || section === "rooms") return { label: section === "rooms" ? "Thêm phòng" : "Thêm bàn", href: `/admin/shop/${shop.id}/${section}` }; + if (section === "promotions") return { label: "Tạo khuyến mãi", href: `/admin/shop/${shop.id}/promotions` }; + return { label: "Mở POS", href: `/pos/${shop.id}/${shop.vertical ?? "cafe"}` }; + } + if (kind === "admin") return { label: "Tạo cửa hàng", href: "/admin/stores/create" }; + if (kind === "staff") return { label: "Check-in", href: "/staff/attendance" }; + if (kind === "marketing") return { label: "Tạo nội dung", href: "/marketing/content" }; + if (kind === "superadmin") return { label: "Feature flag", href: "/superadmin/system/flags" }; + return null; +} + export function buildPortalPayload(kind: PortalKind, input: { shop?: PortalPayload["shop"]; stats?: Record; @@ -276,18 +314,20 @@ export function buildPortalPayload(kind: PortalKind, input: { title?: string; path?: string[]; items?: ListItem[]; + nav?: PortalNavItem[]; }): PortalPayload { const money = new Intl.NumberFormat("vi-VN", { style: "currency", currency: "VND", maximumFractionDigits: 0 }); const stats = input.stats ?? {}; return { title: input.title ?? (kind === "admin" ? "Bảng điều khiển vận hành" : kind === "staff" ? "Ca làm nhân viên" : kind === "marketing" ? "Marketing hub" : "Platform control"), - subtitle: input.shop ? `Đang vận hành ${input.shop.name}` : "Route parity với web-client-tpos-net trong Next MVP.", + subtitle: input.shop ? `Đang vận hành ${input.shop.name}` : "Bảng vận hành TPOS cho cửa hàng, nhân sự, marketing và nền tảng.", shop: input.shop, metrics: input.metrics ?? [ { label: "Doanh thu", value: money.format(Number(stats.todayRevenue ?? stats.revenue ?? 0)), tone: "orange" }, { label: "Đơn hàng", value: Number(stats.orderCount ?? 0), tone: "green" }, { label: "Cửa hàng", value: Number(stats.shopCount ?? 1), tone: "blue" } ], - primary: input.items + primary: input.items, + nav: input.nav }; } diff --git a/microservices/apps/tpos-mvp-next/src/components/TposPosExperience.tsx b/microservices/apps/tpos-mvp-next/src/components/TposPosExperience.tsx index 7ca3a5fb..2058ec91 100644 --- a/microservices/apps/tpos-mvp-next/src/components/TposPosExperience.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/TposPosExperience.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useMemo, useState, useTransition } from "react"; +import { useEffect, useMemo, useState, useTransition } from "react"; import { ArrowLeft, Banknote, @@ -27,12 +27,13 @@ import { UtensilsCrossed } from "lucide-react"; import { posWorkflows, verticals, type VerticalKind } from "./tpos-config"; -import type { OrderSummary, Product, ProductCategory, Shop, TableInfo } from "@/server/domain/types"; +import type { InventoryItem, OrderSummary, Product, ProductCategory, Shop, TableInfo } from "@/server/domain/types"; type CartLine = { product: Product; quantity: number }; type PosTab = "sale" | "history" | "dashboard" | "settings"; type KitchenTicket = Record; type BaristaQueueItem = Record; +type ApiEnvelope = { success?: boolean; data?: T; error?: string }; const currency = new Intl.NumberFormat("vi-VN", { style: "currency", currency: "VND", maximumFractionDigits: 0 }); @@ -45,19 +46,26 @@ const verticalIcons: Record = { retail: ShoppingCart }; -const posNavVerticals = verticals.filter((item) => item.id !== "beauty"); +const posNavVerticals = verticals.filter((item) => item.visibleInPosNav !== false); const paymentMethods = [ - { id: "cash", label: "Tiền mặt", icon: Banknote }, - { id: "card", label: "Thẻ", icon: CreditCard }, - { id: "qr", label: "QR", icon: Smartphone }, - { id: "transfer", label: "Chuyển khoản", icon: Building2 } + { id: "cash", label: "Tiền mặt", icon: Banknote, enabled: true }, + { id: "card", label: "Thẻ", icon: CreditCard, enabled: false }, + { id: "qr", label: "QR", icon: Smartphone, enabled: false }, + { id: "transfer", label: "Chuyển khoản", icon: Building2, enabled: false } +]; + +const paymentChoiceOptions = [ + { id: "cash", route: "cash", label: "Tiền mặt", icon: Banknote, enabled: true }, + { id: "card", route: "card", label: "Thẻ", icon: CreditCard, enabled: false }, + { id: "qr", route: "qr", label: "QR", icon: Smartphone, enabled: false }, + { id: "transfer", route: "transfer", label: "Chuyển khoản", icon: Building2, enabled: false } ]; const posTabs: Array<{ id: PosTab; label: string; icon: typeof Coffee }> = [ { id: "sale", label: "Bán hàng", icon: Coffee }, { id: "history", label: "Lịch sử", icon: History }, - { id: "dashboard", label: "Dashboard", icon: BarChart3 }, + { id: "dashboard", label: "Báo cáo", icon: BarChart3 }, { id: "settings", label: "Cài đặt", icon: Settings } ]; @@ -65,9 +73,11 @@ export function TposPosExperience({ shop, vertical, workflow, + initialTab = "sale", products, categories, tables, + inventory, orders, dashboard, kitchenTickets = [], @@ -76,9 +86,11 @@ export function TposPosExperience({ shop: Shop; vertical: VerticalKind; workflow?: string[]; + initialTab?: PosTab; products: Product[]; categories: ProductCategory[]; tables: TableInfo[]; + inventory: InventoryItem[]; orders: OrderSummary[]; dashboard: Record; kitchenTickets?: KitchenTicket[]; @@ -88,12 +100,13 @@ export function TposPosExperience({ const [categoryId, setCategoryId] = useState("all"); const [query, setQuery] = useState(""); const [selectedTable, setSelectedTable] = useState(tables[0]?.id ?? ""); - const [activeTab, setActiveTab] = useState("sale"); + const [activeTab, setActiveTab] = useState(initialTab); const [paymentMethod, setPaymentMethod] = useState("cash"); const [amountTendered, setAmountTendered] = useState(""); const [voucher, setVoucher] = useState(""); const [discount, setDiscount] = useState(0); const [message, setMessage] = useState(null); + const [dataRevision, setDataRevision] = useState(0); const [isPending, startTransition] = useTransition(); const Icon = verticalIcons[vertical] ?? Coffee; const rawWorkflowSlug = workflow?.[0]; @@ -115,7 +128,7 @@ export function TposPosExperience({ const total = Math.max(0, subtotal - discount); const received = paymentMethod === "cash" ? Number(amountTendered || 0) : total; const change = Math.max(0, received - total); - const canPay = cart.length > 0 && (paymentMethod !== "cash" || received >= total); + const canPay = cart.length > 0 && paymentMethod === "cash" && received >= total; const quickAmounts = [total, Math.ceil(total / 10000) * 10000, Math.ceil(total / 10000) * 10000 + 20000, Math.ceil(total / 10000) * 10000 + 50000] .filter((value, index, all) => value > 0 && all.indexOf(value) === index) .slice(0, 4); @@ -153,6 +166,10 @@ export function TposPosExperience({ async function submitPayment() { if (!canPay) return; + if (paymentMethod !== "cash") { + setMessage("Phương thức này chưa cấu hình cổng thanh toán"); + return; + } setMessage(null); startTransition(async () => { const response = await fetch("/api/bff/pos/orders", { @@ -179,6 +196,7 @@ export function TposPosExperience({ setDiscount(0); setVoucher(""); setMessage(`Thanh toán thành công ${payload.data?.transactionId ?? ""}`); + setDataRevision((current) => current + 1); }); } @@ -219,6 +237,11 @@ export function TposPosExperience({ }) }); if (!kitchenResponse.ok) { + setCart([]); + setAmountTendered(""); + setDiscount(0); + setVoucher(""); + setDataRevision((current) => current + 1); setMessage("Đã lưu order, nhưng chưa gửi được bếp. Kiểm tra Kitchen tickets."); return; } @@ -228,12 +251,13 @@ export function TposPosExperience({ setAmountTendered(""); setDiscount(0); setVoucher(""); + setDataRevision((current) => current + 1); setMessage(vertical === "restaurant" ? "Đã gửi bếp và giữ order tại bàn" : "Đã lưu order F&B cho phòng"); }); } if (workflowSlug) { - return ; + return ; } return ( @@ -361,13 +385,26 @@ export function TposPosExperience({
- {paymentMethods.map((method) => { - const PayIcon = method.icon; - return ( - + {paymentMethods.map((method) => { + const PayIcon = method.icon; + const selected = paymentMethod === method.id; + return ( + ); })}
@@ -401,8 +438,8 @@ export function TposPosExperience({ ) : null} - {activeTab === "history" ? : null} - {activeTab === "dashboard" ? : null} + {activeTab === "history" ? : null} + {activeTab === "dashboard" ? : null} {activeTab === "settings" ? : null} @@ -410,10 +447,13 @@ export function TposPosExperience({ ); } -function HistoryPanel({ orders }: { orders: OrderSummary[] }) { +function HistoryPanel({ shopId, initialOrders, revision }: { shopId: string; initialOrders: OrderSummary[]; revision: number }) { + const [orders, setOrders] = useState(initialOrders); const [historyQuery, setHistoryQuery] = useState(""); const [historyPeriod, setHistoryPeriod] = useState<"today" | "7d" | "30d" | "all">("today"); const [selectedOrderId, setSelectedOrderId] = useState(null); + const [isHistoryLoading, setIsHistoryLoading] = useState(false); + const [historyError, setHistoryError] = useState(null); const normalizedQuery = historyQuery.trim().toLowerCase(); const periodOrders = orders.filter((order) => historyPeriod === "all" || isInHistoryPeriod(order.createdAt, historyPeriod)); const filteredOrders = normalizedQuery @@ -433,6 +473,36 @@ function HistoryPanel({ orders }: { orders: OrderSummary[] }) { const revenue = paidOrders.reduce((sum, order) => sum + order.totalAmount, 0); const selectedOrder = selectedOrderId ? orders.find((order) => order.id === selectedOrderId) : null; + useEffect(() => { + setOrders(initialOrders); + }, [initialOrders]); + + useEffect(() => { + let cancelled = false; + const filter = historyPeriod === "7d" ? "7d" : historyPeriod === "30d" ? "30d" : historyPeriod; + setIsHistoryLoading(true); + setHistoryError(null); + fetch(`/api/bff/orders?shopId=${encodeURIComponent(shopId)}&filter=${filter}&page=1&pageSize=100`) + .then(async (response) => { + const payload = await response.json() as ApiEnvelope<{ items?: OrderSummary[] } | OrderSummary[]>; + if (!response.ok || payload.success === false) throw new Error(payload.error ?? "Không tải được lịch sử"); + const data = payload.data; + return Array.isArray(data) ? data : data?.items ?? []; + }) + .then((items) => { + if (!cancelled) setOrders(items); + }) + .catch((error) => { + if (!cancelled) setHistoryError(error instanceof Error ? error.message : "Không tải được lịch sử"); + }) + .finally(() => { + if (!cancelled) setIsHistoryLoading(false); + }); + return () => { + cancelled = true; + }; + }, [historyPeriod, revision, shopId]); + if (selectedOrder) { return (
@@ -505,6 +575,8 @@ function HistoryPanel({ orders }: { orders: OrderSummary[] }) { {filteredOrders.length} đơn {paidOrders.length} đã thu {currency.format(revenue)} + {isHistoryLoading ? Đang tải : null} + {historyError ? {historyError} : null}
{filteredOrders.map((order) => ( @@ -534,37 +606,89 @@ function HistoryPanel({ orders }: { orders: OrderSummary[] }) { ); } -function DashboardPanel({ dashboard, orders }: { dashboard: Record; orders: OrderSummary[] }) { +function DashboardPanel({ + shopId, + initialDashboard, + initialOrders, + revision +}: { + shopId: string; + initialDashboard: Record; + initialOrders: OrderSummary[]; + revision: number; +}) { const [dashPeriod, setDashPeriod] = useState<"today" | "7d" | "30d">("today"); - const periodOrders = orders.filter((order) => isInDashPeriod(order.createdAt, dashPeriod)); + const [dashboard, setDashboard] = useState>(initialDashboard); + const [dashboardOrders, setDashboardOrders] = useState(initialOrders); + const [isDashboardLoading, setIsDashboardLoading] = useState(false); + const [dashboardError, setDashboardError] = useState(null); + const periodOrders = dashboardOrders.filter((order) => isInDashPeriod(order.createdAt, dashPeriod)); const visibleOrders = periodOrders; - const hasOrderFeed = orders.length > 0; - const revenue = hasOrderFeed ? visibleOrders.reduce((sum, order) => sum + order.totalAmount, 0) : Number(dashboard.revenue ?? 0); - const orderCount = hasOrderFeed ? visibleOrders.length : Number(dashboard.orderCount ?? 0); - const averageTicket = orderCount ? revenue / orderCount : Number(dashboard.averageTicket ?? 0); - const itemCount = visibleOrders.reduce((sum, order) => sum + order.itemCount, 0); - const paymentRows = Object.entries(visibleOrders.reduce>((acc, order) => { + const hasDashboardMetrics = dashboard.revenue !== undefined || dashboard.orderCount !== undefined; + const revenue = hasDashboardMetrics ? Number(dashboard.revenue ?? 0) : visibleOrders.reduce((sum, order) => sum + order.totalAmount, 0); + const orderCount = hasDashboardMetrics ? Number(dashboard.orderCount ?? 0) : visibleOrders.length; + const averageTicket = orderCount ? revenue / orderCount : Number(dashboard.averageTicket ?? dashboard.avgOrderValue ?? 0); + const itemCount = hasDashboardMetrics ? Number(dashboard.itemsSold ?? 0) : visibleOrders.reduce((sum, order) => sum + order.itemCount, 0); + const apiPaymentRows = normalizePaymentRows(dashboard.paymentBreakdown); + const paymentRows = apiPaymentRows.length ? apiPaymentRows : Object.entries(visibleOrders.reduce>((acc, order) => { const method = paymentLabel(order.paymentMethod); acc[method] = acc[method] ?? { count: 0, total: 0 }; acc[method].count += 1; acc[method].total += order.totalAmount; return acc; }, {})).sort((a, b) => b[1].total - a[1].total).slice(0, 4); - const topItems = Object.entries(visibleOrders.flatMap((order) => order.items).reduce>((acc, item) => { + const apiTopItems = normalizeTopItems(dashboard.popularItems); + const topItems = apiTopItems.length ? apiTopItems : Object.entries(visibleOrders.flatMap((order) => order.items).reduce>((acc, item) => { acc[item.productName] = acc[item.productName] ?? { quantity: 0, total: 0 }; acc[item.productName].quantity += item.quantity; acc[item.productName].total += item.totalPrice; return acc; }, {})).sort((a, b) => b[1].total - a[1].total).slice(0, 5); - const hourlyRows = buildHourlyRevenue(visibleOrders); + const apiHourlyRows = normalizeHourlyRows(dashboard.hourlyRevenue); + const hourlyRows = apiHourlyRows.length ? apiHourlyRows : buildHourlyRevenue(visibleOrders); const maxPayment = Math.max(...paymentRows.map(([, value]) => value.total), 1); + useEffect(() => { + setDashboard(initialDashboard); + setDashboardOrders(initialOrders); + }, [initialDashboard, initialOrders]); + + useEffect(() => { + let cancelled = false; + const period = dashPeriod === "7d" ? "7d" : dashPeriod === "30d" ? "30d" : "today"; + setIsDashboardLoading(true); + setDashboardError(null); + fetch(`/api/bff/pos/dashboard?shopId=${encodeURIComponent(shopId)}&period=${period}`) + .then(async (response) => { + const payload = await response.json() as ApiEnvelope>; + if (!response.ok || payload.success === false || !payload.data) throw new Error(payload.error ?? "Không tải được dashboard"); + return payload.data; + }) + .then((data) => { + if (!cancelled) { + setDashboard(data); + setDashboardOrders(normalizeRecentOrders(data.recentOrders)); + } + }) + .catch((error) => { + if (!cancelled) setDashboardError(error instanceof Error ? error.message : "Không tải được dashboard"); + }) + .finally(() => { + if (!cancelled) setIsDashboardLoading(false); + }); + return () => { + cancelled = true; + }; + }, [dashPeriod, revision, shopId]); + return (
Dashboard bán hàng
{formatPeriodLabel(dashPeriod)}
+ {isDashboardLoading ?
Đang tải dữ liệu mới
: null} + {dashboardError ?
{dashboardError}
: null}
{[ @@ -631,6 +755,76 @@ function DashboardPanel({ dashboard, orders }: { dashboard: Record[] { + return Array.isArray(value) ? value.filter((item): item is Record => Boolean(item) && typeof item === "object") : []; +} + +function normalizeTopItems(value: unknown): Array<[string, { quantity: number; total: number }]> { + return recordArray(value) + .map((item) => { + const name = String(item.productName ?? item.name ?? ""); + return [ + name, + { + quantity: toFiniteNumber(item.quantitySold ?? item.quantity ?? item.qty), + total: toFiniteNumber(item.revenue ?? item.total) + } + ] as [string, { quantity: number; total: number }]; + }) + .filter(([name]) => Boolean(name)) + .sort((a, b) => b[1].total - a[1].total) + .slice(0, 5); +} + +function normalizePaymentRows(value: unknown): Array<[string, { count: number; total: number }]> { + const grouped = recordArray(value).reduce>((acc, item) => { + const method = paymentLabel(String(item.method ?? "cash")); + acc[method] = acc[method] ?? { count: 0, total: 0 }; + acc[method].count += toFiniteNumber(item.count); + acc[method].total += toFiniteNumber(item.amount ?? item.total); + return acc; + }, {}); + return Object.entries(grouped) + .sort((a, b) => b[1].total - a[1].total) + .slice(0, 4); +} + +function normalizeHourlyRows(value: unknown) { + const rows = recordArray(value).map((item) => ({ + hour: typeof item.hourLabel === "string" ? item.hourLabel : `${String(toFiniteNumber(item.hour)).padStart(2, "0")}h`, + total: toFiniteNumber(item.revenue ?? item.total), + percent: 0 + })); + const max = Math.max(...rows.map((row) => row.total), 1); + return rows.map((row) => ({ ...row, percent: Math.round(row.total / max * 100) })); +} + +function normalizeRecentOrders(value: unknown): OrderSummary[] { + return recordArray(value).map((item) => ({ + id: String(item.id ?? ""), + shopId: String(item.shopId ?? item.shop_id ?? ""), + shopName: typeof item.shopName === "string" ? item.shopName : null, + tableId: typeof item.tableId === "string" ? item.tableId : null, + tableNumber: typeof item.tableNumber === "string" ? item.tableNumber : null, + statusId: toFiniteNumber(item.statusId ?? item.status_id, 3), + status: String(item.status ?? "Paid"), + totalAmount: toFiniteNumber(item.totalAmount ?? item.total_amount), + discountAmount: toFiniteNumber(item.discountAmount ?? item.discount_amount), + discountType: typeof item.discountType === "string" ? item.discountType : null, + discountReference: typeof item.discountReference === "string" ? item.discountReference : null, + paymentMethod: typeof item.paymentMethod === "string" ? item.paymentMethod : null, + transactionId: typeof item.transactionId === "string" ? item.transactionId : null, + itemCount: toFiniteNumber(item.itemCount ?? item.item_count), + createdAt: String(item.createdAt ?? item.created_at ?? new Date().toISOString()), + items: [] + })); +} + function formatOrderTime(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return "--:--"; @@ -710,18 +904,7 @@ function statusLabel(status: string) { } function paymentLabel(method: string | null) { - switch ((method ?? "cash").toLowerCase()) { - case "card": - return "Thẻ"; - case "qr": - return "QR"; - case "transfer": - return "Chuyển khoản"; - case "wallet": - return "Ví"; - default: - return "Tiền mặt"; - } + return paymentMethodLabel(method); } function SettingsPanel({ shop, vertical }: { shop: Shop; vertical: VerticalKind }) { @@ -754,6 +937,7 @@ function WorkflowScreen({ workflowPath, products, tables, + inventory, orders, dashboard, kitchenTickets, @@ -765,6 +949,7 @@ function WorkflowScreen({ workflowPath: string[]; products: Product[]; tables: TableInfo[]; + inventory: InventoryItem[]; orders: OrderSummary[]; dashboard: Record; kitchenTickets: KitchenTicket[]; @@ -772,11 +957,23 @@ function WorkflowScreen({ }) { const workflow = (posWorkflows[vertical] ?? posWorkflows.shared).find((item) => item.slug === slug) ?? posWorkflows.shared.find((item) => item.slug === slug); const WorkflowIcon = workflow?.icon ?? Coffee; - const contextId = workflowPath[1]; - const contextOrder = contextId ? orders.find((order) => order.id === contextId || order.id.startsWith(contextId)) : undefined; - const contextTable = contextId ? tables.find((table) => table.id === contextId || table.tableNumber === contextId) : undefined; - const paymentWorkflow = slug.includes("payment") || slug === "method-select"; + const contextId = workflowPath[1]; + const contextOrder = contextId ? orders.find((order) => order.id === contextId || order.id.startsWith(contextId)) : undefined; + const contextTable = contextId ? tables.find((table) => table.id === contextId || table.tableNumber === contextId) : undefined; + const methodSelectWorkflow = slug === "method-select"; + const paymentWorkflow = slug.includes("payment") || slug === "receipt" || slug === "tip"; + const cashPaymentWorkflow = slug === "cash-payment"; + const paymentSuccessWorkflow = slug === "payment-success"; + const paymentCollectionWorkflow = paymentWorkflow && slug !== "receipt" && slug !== "tip"; + const contextOrderIsPaid = isPaidOrCompleted(contextOrder); const [tendered, setTendered] = useState(() => String(contextOrder?.totalAmount ?? "")); + const [cancelReason, setCancelReason] = useState("Khách yêu cầu hủy đơn"); + const [voucherCode, setVoucherCode] = useState(""); + const [nextTableStatusId, setNextTableStatusId] = useState("2"); + const [stockInventoryId, setStockInventoryId] = useState(() => inventory[0]?.id ?? ""); + const [stockQuantity, setStockQuantity] = useState("1"); + const [stockNotes, setStockNotes] = useState(""); + const [workflowQuery, setWorkflowQuery] = useState(""); const [workflowMessage, setWorkflowMessage] = useState(null); const [isWorkflowPending, startWorkflowTransition] = useTransition(); @@ -802,8 +999,125 @@ function WorkflowScreen({ }); } + function cancelContextOrder() { + if (!contextOrder) return; + startWorkflowTransition(async () => { + const response = await fetch(`/api/bff/orders/${contextOrder.id}/cancel`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ shopId: shop.id, reason: cancelReason }) + }); + const payload = await response.json() as ApiEnvelope; + setWorkflowMessage(response.ok && payload.success ? "Đã hủy đơn và ghi nhận lý do trong DB" : payload.error ?? "Không thể hủy đơn"); + }); + } + + function validateWorkflowVoucher() { + const code = voucherCode.trim(); + if (!code) { + setWorkflowMessage("Vui lòng nhập mã voucher để kiểm tra"); + return; + } + startWorkflowTransition(async () => { + const response = await fetch(`/api/bff/vouchers/validate/${encodeURIComponent(code)}?shopId=${encodeURIComponent(shop.id)}`); + const payload = await response.json() as ApiEnvelope<{ valid?: boolean; message?: string; discountType?: string; discountValue?: number }>; + if (!response.ok || !payload.success) { + setWorkflowMessage(payload.error ?? "Không thể kiểm tra voucher"); + return; + } + const result = payload.data; + setWorkflowMessage(result?.valid + ? `Voucher hợp lệ: ${result.discountType ?? "discount"} ${result.discountValue ?? 0}` + : result?.message ?? "Voucher không hợp lệ"); + }); + } + + function updateContextTableStatus() { + if (!contextTable) return; + startWorkflowTransition(async () => { + const response = await fetch(`/api/bff/tables/${contextTable.id}/status`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ shopId: shop.id, statusId: Number(nextTableStatusId) }) + }); + const payload = await response.json() as ApiEnvelope; + setWorkflowMessage(response.ok && payload.success ? "Đã cập nhật trạng thái bàn/phòng trong DB" : payload.error ?? "Không thể cập nhật bàn/phòng"); + }); + } + + function updateShiftAttendance(action: "check-in" | "check-out") { + startWorkflowTransition(async () => { + const response = await fetch(`/api/bff/staff/me/attendance/${action}`, { method: "POST" }); + const payload = await response.json() as ApiEnvelope; + setWorkflowMessage(response.ok && payload.success + ? (action === "check-in" ? "Đã check-in ca làm trong DB" : "Đã check-out ca làm trong DB") + : payload.error ?? "Không thể cập nhật ca làm"); + }); + } + + function updateWorkflowStock(action: "stock-in" | "stock-out") { + const quantity = Number(stockQuantity); + if (!stockInventoryId) { + setWorkflowMessage("Chọn mặt hàng tồn kho trước khi ghi nhận"); + return; + } + if (!Number.isFinite(quantity) || quantity <= 0) { + setWorkflowMessage("Số lượng phải lớn hơn 0"); + return; + } + startWorkflowTransition(async () => { + const response = await fetch(`/api/bff/inventory/${action}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + shopId: shop.id, + inventoryId: stockInventoryId, + quantity, + notes: stockNotes.trim() || `${action} from POS workflow` + }) + }); + const payload = await response.json() as ApiEnvelope; + if (!response.ok || !payload.success) { + setWorkflowMessage(payload.error ?? "Không thể cập nhật tồn kho"); + return; + } + const itemName = payload.data?.name ?? payload.data?.productName ?? "Tồn kho"; + setWorkflowMessage(`${action === "stock-in" ? "Đã nhập kho" : "Đã xuất kho"} ${itemName}`); + }); + } + const workflows = uniqueWorkflows(vertical); - const workflowRows = workflowDataRows(slug, { orders, tables, kitchenTickets, baristaQueue }); + if (!workflow) { + return ( +
+
+
+ + aPOS POS + Workflow không tồn tại +
+
Online
+
+
+
+
+
+ KHÔNG CÓ TRONG TPOS +

Workflow không tồn tại

+

Route này không nằm trong registry POS của TPOS MVP, nên hệ thống không hiển thị bảng dữ liệu giả.

+
+ Quay lại POS +
+
+
+
+ ); + } + const workflowRows = workflowDataRows(slug, { orders, tables, products, inventory, kitchenTickets, baristaQueue }); + const visibleWorkflowRows = workflowRows.filter((row) => { + const needle = workflowQuery.trim().toLowerCase(); + return !needle || `${row.title} ${row.meta} ${row.value}`.toLowerCase().includes(needle); + }); return (
@@ -842,10 +1156,10 @@ function WorkflowScreen({
- {contextOrder || contextTable || paymentWorkflow ? ( -
-
- {paymentWorkflow ? "PAYMENT CONTEXT" : "ROUTE CONTEXT"} + {contextOrder || contextTable || paymentWorkflow || methodSelectWorkflow ? ( +
+
+ {paymentWorkflow || methodSelectWorkflow ? "PAYMENT CONTEXT" : "ROUTE CONTEXT"}

{contextOrder ? `Đơn ${contextOrder.id.slice(0, 8).toUpperCase()}` : contextTable ? `Bàn/phòng ${contextTable.tableNumber}` : workflow?.title ?? slug}

{contextOrder @@ -855,36 +1169,270 @@ function WorkflowScreen({ : "Route giữ nguyên slug, ID và query để workflow thanh toán/dialog có thể nối vào BFF thật."}

- {contextOrder ? Xem đơn hàng : null} + {contextOrder ? Xem đơn hàng : null} +
+ ) : null} + {methodSelectWorkflow ? ( +
+
+ CHỌN PHƯƠNG THỨC +

{contextOrder ? `Thanh toán đơn ${contextOrder.id.slice(0, 8).toUpperCase()}` : "Chọn phương thức thanh toán"}

+

{contextOrder ? `Tổng cần thu ${currency.format(contextOrder.totalAmount)}` : "Chọn đơn hàng từ POS hoặc truyền orderId để tiếp tục thanh toán."}

+
+
+ {paymentChoiceOptions.map((method) => { + const MethodIcon = method.icon; + const disabled = !contextOrder || !method.enabled; + const href = contextOrder ? paymentChoiceHref(shop.id, vertical, method.route, contextOrder.id) : "#"; + return disabled ? ( + + ) : ( + + + {method.label} + + ); + })} +
+
+ ) : null} + {paymentSuccessWorkflow ? ( +
+
+ THANH TOÁN THÀNH CÔNG +

{contextOrderIsPaid ? `Đã hoàn tất đơn ${contextOrder!.id.slice(0, 8).toUpperCase()}` : "Cần đơn đã thanh toán"}

+

{contextOrderIsPaid ? `${contextOrder!.itemCount} món · ${currency.format(contextOrder!.totalAmount)} · ${paymentMethodLabel(contextOrder!.paymentMethod)}` : "Route success phải có orderId đã Paid/Completed. Không hiển thị thành công giả cho đơn thiếu hoặc chưa thu tiền."}

+
+ {!contextOrderIsPaid ?
Thiếu payment ledger hoặc đơn chưa thanh toán.
: null} +
+ + + Lịch sử đơn + + + + Tiếp tục bán + +
+
+ ) : null} + {slug === "receipt" ? ( +
+
+ BIÊN LAI +

{contextOrderIsPaid ? `Biên lai đơn ${contextOrder!.id.slice(0, 8).toUpperCase()}` : "Chưa có biên lai hợp lệ"}

+

{contextOrderIsPaid ? `${contextOrder!.itemCount} món · ${currency.format(contextOrder!.totalAmount)} · ${paymentMethodLabel(contextOrder!.paymentMethod)}` : "Chỉ in lại biên lai cho đơn đã thanh toán. Không dựng receipt giả."}

+
+ {contextOrderIsPaid ? Mở lịch sử đơn :
Thiếu orderId Paid/Completed.
}
) : null} - {paymentWorkflow && contextOrder ? ( + {slug === "tip" ? (
- PAYMENT ACTION -

Thu tiền đơn {contextOrder.id.slice(0, 8).toUpperCase()}

-

Tổng cần thu {currency.format(contextOrder.totalAmount)} qua {paymentMethodFromSlug(slug)}.

+ TIP +

Tip chưa cấu hình ledger

+

TPOS gốc có route tip riêng. MVP Next giữ route này nhưng chưa ghi nhận tip cho tới khi có ledger ca và phân bổ nhân viên.

- - +
Không ghi nhận thành công giả.
+
+ ) : null} + {paymentCollectionWorkflow && !paymentSuccessWorkflow && contextOrder ? ( +
+
+ THANH TOÁN +

Thu tiền đơn {contextOrder.id.slice(0, 8).toUpperCase()}

+

+ {cashPaymentWorkflow + ? `Tổng cần thu ${currency.format(contextOrder.totalAmount)} qua tiền mặt.` + : "Workflow này cần ledger hoặc adapter thanh toán riêng, chưa được phép ghi nhận như tiền mặt."} +

+
+ {cashPaymentWorkflow ? ( + <> + + + + ) : ( +
Chưa cấu hình workflow thanh toán này. Không ghi nhận fake success.
+ )} {workflowMessage ?
{workflowMessage}
: null}
) : null} + {slug === "discount" ? ( +
+
+ GIẢM GIÁ +

Kiểm tra voucher thật

+

Workflow này gọi BFF voucher validate theo shop. Việc áp vào hóa đơn chỉ bật khi có service chỉnh sửa đơn persisted.

+
+ + + {workflowMessage ?
{workflowMessage}
: null} +
+ ) : null} + {slug === "void-refund" ? ( +
+
+ HỦY / HOÀN TIỀN +

{contextOrder ? `Hủy đơn ${contextOrder.id.slice(0, 8).toUpperCase()}` : "Chọn đơn để hủy"}

+

{contextOrder ? "Hành động này gọi service hủy đơn thật và ghi lý do vào DB. Hoàn tiền ledger sẽ bật khi có bảng refund riêng." : "Truyền orderId trên route để thao tác, ví dụ /dialog/void-refund?orderId=..."}

+
+ {contextOrder ? ( + <> + + + + ) :
Thiếu orderId nên không ghi thay đổi.
} + {contextOrderIsPaid ?
Đơn đã thanh toán cần refund ledger riêng, không hủy trực tiếp.
: null} + {workflowMessage ?
{workflowMessage}
: null} +
+ ) : null} + {slug === "table-transfer" ? ( +
+
+ BÀN / PHÒNG +

{contextTable ? `Cập nhật ${vertical === "karaoke" ? "phòng" : "bàn"} ${contextTable.tableNumber}` : "Chọn bàn/phòng"}

+

Chuyển order giữa bàn/phòng chưa có service riêng. Màn này chỉ cập nhật trạng thái bàn/phòng thật, không ghi thành công giả.

+
+ {contextTable ? ( + <> + + + + ) :
Thiếu tableId/roomId nên không ghi thay đổi.
} + {workflowMessage ?
{workflowMessage}
: null} +
+ ) : null} + {slug === "product-search" || slug === "stock-check" ? ( +
+
+ {slug === "stock-check" ? "TỒN KHO" : "CATALOG"} +

{slug === "stock-check" ? "Kiểm tồn kho" : "Tra SKU/giá"}

+

Dữ liệu đọc trực tiếp từ MVP DB. Stock mutation vẫn yêu cầu quyền admin.

+
+ + Mở quản trị +
+ ) : null} + {slug === "stock-in" || slug === "stock-out" ? ( +
+
+ TỒN KHO +

{slug === "stock-in" ? "Nhập kho" : "Xuất kho"}

+

Workflow này gọi BFF inventory thật và ghi transaction trong MVP DB.

+
+ + + + + {workflowMessage ?
{workflowMessage}
: null} +
+ ) : null} + {slug === "stock-transfer" ? ( +
+
+ CHUYỂN KHO +

Chưa có mô hình chuyển kho

+

TPOS gốc có route chuyển kho riêng. MVP Next cần bảng kho nguồn/đích và transaction transfer trước khi cho ghi nhận.

+
+
Không ghi nhận thành công giả.
+
+ ) : null} + {["order-edit", "split-bill", "cash-drawer", "shift"].includes(slug) ? ( +
+
+ THAO TÁC THẬT +

{workflow?.title ?? slug}

+

+ {slug === "order-edit" || slug === "split-bill" + ? "Chưa có service chỉnh sửa/tách hóa đơn persisted nên workflow không ghi fake success." + : slug === "shift" + ? "Check-in/check-out dùng staff attendance thật. Mở/đóng ca bán và cash drawer cần ledger ca riêng." + : "Cash drawer cần device registry và drawer event table trước khi cho mở két."} +

+
+ {slug === "order-edit" && contextOrder ? Xem lịch sử đơn : null} + {slug === "shift" ? ( +
+ + +
+ ) : null} + {slug === "shift" && workflowMessage ?
{workflowMessage}
:
Chưa cấu hình backend cho action còn lại. Không ghi nhận thành công giả.
} +
+ ) : null} + {slug === "quick-sale" ? ( +
+
+ BÁN NHANH +

Bán nhanh qua POS chính

+

Luồng bán nhanh dùng cart/POS chính để ghi đơn thật, payment thật theo cash hoặc deferred order.

+
+ Mở màn bán hàng +
+ ) : null}
- {workflowRows.slice(0, 10).map((row) => ( + {visibleWorkflowRows.slice(0, 10).map((row) => (
{row.title} {row.meta} {row.value}
))} - {workflowRows.length === 0 ?
Chưa có dữ liệu vận hành cho workflow này
: null} + {visibleWorkflowRows.length === 0 ?
Chưa có dữ liệu vận hành cho workflow này
: null}
@@ -923,8 +1471,8 @@ const workflowAliases: Partial = { "gift-card-payment": "gift-card", "partial-payment": "partial", "payment-pending": "pending", - "payment-success": "success" + "payment-success": "success", + receipt: "receipt", + tip: "tip" }; const dialogWorkflowRoutes: Record = { @@ -959,7 +1509,9 @@ const dialogWorkflowRoutes: Record = { "table-transfer": "table-transfer", "split-bill": "split-bill", "void-refund": "void-refund", - "stock-check": "stock-out", + "stock-in": "stock-in", + "stock-out": "stock-out", + "stock-transfer": "stock-transfer", "product-search": "price-check" }; @@ -976,13 +1528,17 @@ function normalizeWorkflowSlug(vertical: VerticalKind, slug: string) { function workflowHref(shopId: string, vertical: VerticalKind, slug: string) { const canonicalSlug = normalizeWorkflowSlug(vertical, slug); - const query = `?vertical=${vertical}`; - if (paymentWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/payment/${paymentWorkflowRoutes[canonicalSlug]}${query}`; - if (dialogWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/dialog/${dialogWorkflowRoutes[canonicalSlug]}${query}`; - if (operationWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/operations/${operationWorkflowRoutes[canonicalSlug]}${query}`; + const verticalQuery = `?vertical=${encodeURIComponent(vertical)}`; + if (paymentWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/payment/${paymentWorkflowRoutes[canonicalSlug]}${verticalQuery}`; + if (dialogWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/dialog/${dialogWorkflowRoutes[canonicalSlug]}${verticalQuery}`; + if (operationWorkflowRoutes[canonicalSlug]) return `/pos/${shopId}/operations/${operationWorkflowRoutes[canonicalSlug]}${verticalQuery}`; return `/pos/${shopId}/${vertical}/${canonicalSlug}`; } +function paymentChoiceHref(shopId: string, vertical: VerticalKind, route: string, orderId: string) { + return `/pos/${shopId}/payment/${route}/${encodeURIComponent(orderId)}?vertical=${encodeURIComponent(vertical)}`; +} + function paymentMethodFromSlug(slug: string) { if (slug.includes("card")) return "card"; if (slug.includes("qr")) return "qr"; @@ -991,9 +1547,31 @@ function paymentMethodFromSlug(slug: string) { return "cash"; } +function paymentMethodLabel(method?: string | null) { + const normalized = String(method ?? "").toLowerCase(); + if (normalized === "cash") return "Tiền mặt"; + if (normalized === "customer_order") return "QR khách hàng"; + if (normalized === "kitchen_order") return "Bếp"; + if (normalized === "room_fnb") return "Phòng karaoke"; + if (normalized === "gift_card") return "Thẻ quà tặng"; + if (normalized === "card") return "Thẻ"; + if (normalized === "qr") return "QR"; + if (normalized === "transfer" || normalized === "bank_transfer") return "Chuyển khoản"; + if (normalized === "wallet") return "Ví"; + if (!normalized) return "Chưa thanh toán"; + return method ?? "Khác"; +} + +function isPaidOrCompleted(order?: OrderSummary | null) { + if (!order) return false; + return order.statusId === 3 || order.statusId === 5 || /paid|complete|đã thanh toán|hoàn tất/i.test(order.status); +} + function workflowDataRows(slug: string, data: { orders: OrderSummary[]; tables: TableInfo[]; + products: Product[]; + inventory: InventoryItem[]; kitchenTickets: KitchenTicket[]; baristaQueue: BaristaQueueItem[]; }) { @@ -1021,6 +1599,35 @@ function workflowDataRows(slug: string, data: { value: table.status })); } + if (slug === "stock-check" || slug === "stock-in" || slug === "stock-out" || slug === "stock-transfer") { + return data.inventory.map((item) => ({ + id: item.id, + title: item.name ?? item.productName ?? "Tồn kho", + meta: `${item.availableQuantity} ${item.unit} khả dụng · đặt lại ${item.reorderLevel}`, + value: item.quantity <= item.reorderLevel ? "Sắp hết" : "OK" + })); + } + if (slug === "product-search") { + return data.products.map((product) => ({ + id: product.id, + title: product.name, + meta: product.sku ?? product.barcode ?? product.categoryName ?? "Catalog", + value: currency.format(product.price) + })); + } + if (slug === "cash-drawer" || slug === "shift") { + return []; + } + if (slug === "pending-orders") { + return data.orders + .filter((order) => !/paid|cancel|hủy/i.test(order.status) && order.statusId !== 3) + .map((order) => ({ + id: order.id, + title: order.tableNumber ? `Bàn/phòng ${order.tableNumber}` : order.id.slice(0, 8).toUpperCase(), + meta: `${order.itemCount} món · ${order.status}`, + value: currency.format(order.totalAmount) + })); + } return data.orders.map((order) => ({ id: order.id, title: order.tableNumber ? `Bàn/phòng ${order.tableNumber}` : order.id.slice(0, 8).toUpperCase(), diff --git a/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx b/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx index 87792e4c..c6f48e2e 100644 --- a/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx @@ -17,11 +17,11 @@ import { } from "lucide-react"; const verticals = [ - { label: "Cafe", icon: Coffee }, - { label: "Nhà hàng & F&B", icon: UtensilsCrossed }, - { label: "Karaoke", icon: Mic }, - { label: "TMV/Spa", icon: Sparkles }, - { label: "Bán lẻ", icon: ShoppingBag } + { label: "Cafe", desc: "Order nhanh, pha chế, ca bán", icon: Coffee }, + { label: "Nhà hàng & F&B", desc: "Bàn, bếp, thanh toán tách/gộp", icon: UtensilsCrossed }, + { label: "Karaoke", desc: "Phòng, giờ hát, dịch vụ đi kèm", icon: Mic }, + { label: "TMV/Spa", desc: "Lịch hẹn, liệu trình, khách hàng", icon: Sparkles }, + { label: "Bán lẻ", desc: "Sản phẩm, tồn kho, khách thân thiết", icon: ShoppingBag } ]; const features = [ @@ -42,37 +42,46 @@ export function TposPublicLanding({ variant = "home" }: { variant?: "home" | "pr return (
- +
-
+
+ Chọn ngành hàng + Vào portal +
- {verticals.map(({ label, icon: Icon }) => ( + {verticals.map(({ label, desc, icon: Icon }) => ( {label} +

{desc}

))}
+ {isProject ? : } +
+ ); +} + +function ProjectSections() { + return ( + <>
TPOS Modules @@ -169,20 +191,34 @@ export function TposPublicLanding({ variant = "home" }: { variant?: "home" | "pr Vào admin demo
- + ); } -function PublicNav() { +function HomePortalCta() { + return ( +
+
+ + Đã có tài khoản TPOS? +
+ Mở portal đăng nhập +
+ ); +} + +function PublicNav({ variant }: { variant: "home" | "project" }) { + const isProject = variant === "project"; + return (