diff --git a/.opencode/openwork/inbox/chat-attachments/97bf0aa496685401-AI.xlsx b/.opencode/openwork/inbox/chat-attachments/97bf0aa496685401-AI.xlsx new file mode 100644 index 0000000..6171beb Binary files /dev/null and b/.opencode/openwork/inbox/chat-attachments/97bf0aa496685401-AI.xlsx differ diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/4c0a7bbb-215e-4846-829c-1a3561af1c12-deployment-hyq204tdb0sbkisvotvcoxjk-all-logs-2026-08-09-00-27-01.txt b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/4c0a7bbb-215e-4846-829c-1a3561af1c12-deployment-hyq204tdb0sbkisvotvcoxjk-all-logs-2026-08-09-00-27-01.txt new file mode 100644 index 0000000..a9d2995 --- /dev/null +++ b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/4c0a7bbb-215e-4846-829c-1a3561af1c12-deployment-hyq204tdb0sbkisvotvcoxjk-all-logs-2026-08-09-00-27-01.txt @@ -0,0 +1,227 @@ +2026-Aug-09 00:23:01.909052 Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost). +2026-Aug-09 00:23:01.959521 Starting deployment of http://forgejo-b82b4lpyyzyg40thdsjdd38i.163.245.214.68.sslip.io/David/MEGAINFRA-WEBAPP.git:@\(.*\):#https://\1/#g" '\''/artifacts/hyq204tdb0sbkisvotvcoxjk'\''/.gitmodules || true && git -c http.version=HTTP/1.1 submodule sync && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 submodule update --init --recursive --depth=1; fi && cd '\''/artifacts/hyq204tdb0sbkisvotvcoxjk'\'' && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 lfs pull' +2026-Aug-09 00:@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 00:23:21.200991 [DEBUG] #3 resolve docker.io/docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e 0.0s done +2026-Aug-09 00:23:21.200991 [DEBUG] #3 sha256:b5f3b260a9678e1d83d2fce86eeddf79420b79147eaba2a25986f47133d73720 482B / 482B done +2026-Aug-09 00:23:21.200991 [DEBUG] #3 sha256:68ebc061390d9a7d6e194f9d58309c754a53cb8b4e3b0d89593920f42f4a9da4 1.26kB / 1.26kB done +2026-Aug-09 00:23:21.200991 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 0B / 11.98MB 0.1s +2026-Aug-09 00:23:21.200991 [DEBUG] #3 sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e 8.40kB / 8.40kB done +2026-Aug-09 00:23:21.601003 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 1.05MB / 11.98MB 0.5s +2026-Aug-09 00:23:21.701293 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 2.10MB / 11.98MB 0.6s +2026-Aug-09 00:23:21.809998 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 4.19MB / 11.98MB 0.7s +2026-Aug-09 00:23:22.005677 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 6.29MB / 11.98MB 0.9s +2026-Aug-09 00:23:22.101507 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 7.34MB / 11.98MB 1.0s +2026-Aug-09 00:23:22.301175 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 9.44MB / 11.98MB 1.2s +2026-Aug-09 00:23:22.502023 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 11.98MB / 11.98MB 1.4s +2026-Aug-09 00:23:22.666058 [DEBUG] #3 extracting sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 +2026-Aug-09 00:23:22.768008 [DEBUG] #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 11.98MB / 11.98MB 1.5s done +2026-Aug-09 00:23:23.041840 [DEBUG] #3 extracting sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 0.3s done +2026-Aug-09 00:23:23.041840 [DEBUG] #3 DONE 2.0s +2026-Aug-09 00:23:23.296406 [DEBUG] #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 00:23:26.399481 [DEBUG] #4 DONE 2.9s +2026-Aug-09 00:23:26.399481 [DEBUG] +2026-Aug-09 00:23:26.399481 [DEBUG] #5 [internal] load .dockerignore +2026-Aug-09 00:23:26.399481 [DEBUG] #5 transferring context: 780B done +2026-Aug-09 00:23:26.399481 [DEBUG] #5 DONE 0.1s +2026-Aug-09 00:23:26.724389 [DEBUG] #6 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 00:23:26.724389 [DEBUG] #6 resolve docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 0.0s done +2026-Aug-09 00:23:26.856268 [DEBUG] #6 sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 7.67kB / 7.67kB done +2026-Aug-09 00:23:26.856268 [DEBUG] #6 sha256:afdf98210b07b586eb71fa22ba2e432e058e4cd1304d31ed60888755b8c865fb 1.72kB / 1.72kB done +2026-Aug-09 00:23:26.856268 [DEBUG] #6 sha256:11cedc39e663e7c5d5cb9cc77a461a0d2adc25537b94e6831a6108f09cb2001b 6.52kB / 6.52kB done +2026-Aug-09 00:23:26.856268 [DEBUG] #6 DONE 0.3s +2026-Aug-09 00:23:26.856268 [DEBUG] +2026-Aug-09 00:23:26.856268 [DEBUG] #7 [internal] load build context +2026-Aug-09 00:23:26.856268 [DEBUG] #7 transferring context: 683.75kB 0.1s done +2026-Aug-09 00:23:26.856268 [DEBUG] #7 DONE 0.2s +2026-Aug-09 00:23:26.985511 [DEBUG] #8 [runner 2/6] WORKDIR /app +2026-Aug-09 00:23:26.985511 [DEBUG] #8 DONE 0.1s +2026-Aug-09 00:23:26.985511 [DEBUG] +2026-Aug-09 00:23:26.985511 [DEBUG] #9 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 00:23:27.501315 [DEBUG] #9 ... +2026-Aug-09 00:23:27.501315 [DEBUG] +2026-Aug-09 00:23:27.501315 [DEBUG] #10 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 00:23:27.501315 [DEBUG] #10 DONE 0.5s +2026-Aug-09 00:23:27.649321 [DEBUG] #9 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 00:23:29.544546 [DEBUG] #9 2.682 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 00:23:29.710634 [DEBUG] #9 2.847 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 00:23:29.874415 [DEBUG] #9 3.012 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 00:23:30.043974 [DEBUG] #9 3.182 OK: 11.0 MiB in 21 packages +2026-Aug-09 00:23:30.183605 [DEBUG] #9 DONE 3.3s +2026-Aug-09 00:23:30.298352 [DEBUG] #11 [deps 3/5] WORKDIR /app +2026-Aug-09 00:23:30.298352 [DEBUG] #11 DONE 0.1s +2026-Aug-09 00:23:30.469332 [DEBUG] #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 00:23:30.512418 [DEBUG] #12 DONE 0.2s +2026-Aug-09 00:23:30.720961 [DEBUG] #13 [deps 5/5] RUN npm ci --no-audit --no-fund +2026-Aug-09 00:23:41.768774 [DEBUG] #13 11.21 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 00:23:43.780782 [DEBUG] #13 13.23 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 00:23:43.883361 [DEBUG] #13 13.33 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 00:23:57.071969 [DEBUG] #13 26.52 npm warn deprecated recharts@2.15.4: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide +2026-Aug-09 00:@1.0.0 postinstall +2026-Aug-09 00:24:28.719823 [DEBUG] #13 58.16 > node scripts/patch-next-dev-blocker.cjs +2026-Aug-09 00:24:28.719823 [DEBUG] #13 58.16 +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 node:internal/modules/cjs/loader:1210 +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 throw err; +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 ^ +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 Error: Cannot find module '/app/scripts/patch-next-dev-blocker.cjs' +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15) +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 at Module._load (node:internal/modules/cjs/loader:1038:27) +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 at node:internal/main/run_main_module:28:49 { +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 code: 'MODULE_NOT_FOUND', +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 requireStack: [] +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 } +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 +2026-Aug-09 00:24:28.864640 [DEBUG] #13 58.31 Node.js v20.20.2 +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm error code 1 +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm error path /app +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm error command failed +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm error command sh -c node scripts/patch-next-dev-blocker.cjs +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm notice +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 00:@12.0.2 +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm notice +2026-Aug-09 00:24:29.049282 [DEBUG] #13 58.34 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-09T00_23_31_502Z-debug-0.log +2026-Aug-09 00:24:29.697366 [DEBUG] #13 ERROR: process "/bin/sh -c npm ci --no-audit --no-fund" did not complete successfully: exit code: 1 +2026-Aug-09 00:24:29.844583 [DEBUG] ------ +2026-Aug-09 00:24:29.844583 [DEBUG] > [deps 5/5] RUN npm ci --no-audit --no-fund: +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm error code 1 +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm error path /app +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm error command failed +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm error command sh -c node scripts/patch-next-dev-blocker.cjs +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm notice +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 00:@12.0.2 +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm notice +2026-Aug-09 00:24:29.844583 [DEBUG] 58.34 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-09T00_23_31_502Z-debug-0.log +2026-Aug-09 00:24:29.844583 [DEBUG] ------ +2026-Aug-09 00:24:29.869776 [DEBUG] Dockerfile:30 +2026-Aug-09 00:24:29.869776 [DEBUG] -------------------- +2026-Aug-09 00:24:29.869776 [DEBUG] 28 | # as dependencies don't change. +2026-Aug-09 00:24:29.869776 [DEBUG] 29 | COPY package.json package-lock.json* ./ +2026-Aug-09 00:24:29.869776 [DEBUG] 30 | >>> RUN npm ci --no-audit --no-fund +2026-Aug-09 00:24:29.869776 [DEBUG] 31 | +2026-Aug-09 00:24:29.869776 [DEBUG] 32 | # --------------------------------------------------------------------------- +2026-Aug-09 00:24:29.869776 [DEBUG] -------------------- +2026-Aug-09 00:24:29.869776 [DEBUG] ERROR: failed to build: failed to solve: process "/bin/sh -c npm ci --no-audit --no-fund" did not complete successfully: exit code: 1 +2026-Aug-09 00:24:29.909663 [DEBUG] exit status 1 +2026-Aug-09 00:24:30.046605 ======================================== +2026-Aug-09 00:24:30.064835 Deployment failed: Command execution failed (exit code 1): docker exec hyq204tdb0sbkisvotvcoxjk bash -c 'bash /artifacts/build.sh' +2026-Aug-09 00:24:30.064835 Error: #0 building with "default" instance using docker driver +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #1 [internal] load build definition from Dockerfile +2026-Aug-09 00:24:30.064835 #1 transferring dockerfile: 3.58kB 0.0s done +2026-Aug-09 00:24:30.064835 #1 DONE 0.1s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #2 resolve image config for docker-image://docker.io/docker/dockerfile:1.7 +2026-Aug-09 00:24:30.064835 #2 DONE 4.0s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #3 docker-image://docker.io/docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 00:24:30.064835 #3 resolve docker.io/docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e 0.0s done +2026-Aug-09 00:24:30.064835 #3 sha256:b5f3b260a9678e1d83d2fce86eeddf79420b79147eaba2a25986f47133d73720 482B / 482B done +2026-Aug-09 00:24:30.064835 #3 sha256:68ebc061390d9a7d6e194f9d58309c754a53cb8b4e3b0d89593920f42f4a9da4 1.26kB / 1.26kB done +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 0B / 11.98MB 0.1s +2026-Aug-09 00:24:30.064835 #3 sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e 8.40kB / 8.40kB done +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 1.05MB / 11.98MB 0.5s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 2.10MB / 11.98MB 0.6s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 4.19MB / 11.98MB 0.7s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 6.29MB / 11.98MB 0.9s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 7.34MB / 11.98MB 1.0s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 9.44MB / 11.98MB 1.2s +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 11.98MB / 11.98MB 1.4s +2026-Aug-09 00:24:30.064835 #3 extracting sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 +2026-Aug-09 00:24:30.064835 #3 sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 11.98MB / 11.98MB 1.5s done +2026-Aug-09 00:24:30.064835 #3 extracting sha256:96918c57e42509b97f10c074d80672ecdbd3bb7dcd38c1bd95960cf291207416 0.3s done +2026-Aug-09 00:24:30.064835 #3 DONE 2.0s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 00:24:30.064835 #4 DONE 2.9s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #5 [internal] load .dockerignore +2026-Aug-09 00:24:30.064835 #5 transferring context: 780B done +2026-Aug-09 00:24:30.064835 #5 DONE 0.1s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #6 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 00:24:30.064835 #6 resolve docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 0.0s done +2026-Aug-09 00:24:30.064835 #6 sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 7.67kB / 7.67kB done +2026-Aug-09 00:24:30.064835 #6 sha256:afdf98210b07b586eb71fa22ba2e432e058e4cd1304d31ed60888755b8c865fb 1.72kB / 1.72kB done +2026-Aug-09 00:24:30.064835 #6 sha256:11cedc39e663e7c5d5cb9cc77a461a0d2adc25537b94e6831a6108f09cb2001b 6.52kB / 6.52kB done +2026-Aug-09 00:24:30.064835 #6 DONE 0.3s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #7 [internal] load build context +2026-Aug-09 00:24:30.064835 #7 transferring context: 683.75kB 0.1s done +2026-Aug-09 00:24:30.064835 #7 DONE 0.2s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #8 [runner 2/6] WORKDIR /app +2026-Aug-09 00:24:30.064835 #8 DONE 0.1s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #9 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 00:24:30.064835 #9 ... +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #10 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 00:24:30.064835 #10 DONE 0.5s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #9 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 00:24:30.064835 #9 2.682 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 00:24:30.064835 #9 2.847 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 00:24:30.064835 #9 3.012 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 00:24:30.064835 #9 3.182 OK: 11.0 MiB in 21 packages +2026-Aug-09 00:24:30.064835 #9 DONE 3.3s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #11 [deps 3/5] WORKDIR /app +2026-Aug-09 00:24:30.064835 #11 DONE 0.1s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 00:24:30.064835 #12 DONE 0.2s +2026-Aug-09 00:24:30.064835 +2026-Aug-09 00:24:30.064835 #13 [deps 5/5] RUN npm ci --no-audit --no-fund +2026-Aug-09 00:24:30.064835 #13 11.21 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 00:24:30.064835 #13 13.23 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 00:24:30.064835 #13 13.33 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 00:24:30.064835 #13 26.52 npm warn deprecated recharts@2.15.4: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide +2026-Aug-09 00:@1.0.0 postinstall +2026-Aug-09 00:24:30.064835 #13 58.16 > node scripts/patch-next-dev-blocker.cjs +2026-Aug-09 00:24:30.064835 #13 58.16 +2026-Aug-09 00:24:30.064835 #13 58.31 node:@12.0.2 +2026-Aug-09 00:24:30.064835 #13 58.34 npm notice +2026-Aug-09 00:24:30.064835 #13 58.34 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-09T00_23_31_502Z-debug-0.log +2026-Aug-09 00:24:30.064835 #13 ERROR: process "/bin/sh -c npm ci --no-audit --no-fund" did not complete successfully: exit code: 1 +2026-Aug-09 00:24:30.064835 ------ +2026-Aug-09 00:24:30.064835 > [deps 5/5] RUN npm ci --no-audit --no-fund: +2026-Aug-09 00:24:30.064835 58.34 npm error code 1 +2026-Aug-09 00:24:30.064835 58.34 npm error path /app +2026-Aug-09 00:24:30.064835 58.34 npm error command failed +2026-Aug-09 00:24:30.064835 58.34 npm error command sh -c node scripts/patch-next-dev-blocker.cjs +2026-Aug-09 00:24:30.064835 58.34 npm notice +2026-Aug-09 00:24:30.064835 58.34 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 00:24:30.064835 58.34 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 00:@12.0.2 +2026-Aug-09 00:24:30.064835 58.34 npm notice +2026-Aug-09 00:24:30.064835 58.34 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-09T00_23_31_502Z-debug-0.log +2026-Aug-09 00:24:30.064835 ------ +2026-Aug-09 00:24:30.064835 Dockerfile:30 +2026-Aug-09 00:24:30.064835 -------------------- +2026-Aug-09 00:24:30.064835 28 | # as dependencies don't change. +2026-Aug-09 00:24:30.064835 29 | COPY package.json package-lock.json* ./ +2026-Aug-09 00:24:30.064835 30 | >>> RUN npm ci --no-audit --no-fund +2026-Aug-09 00:24:30.064835 31 | +2026-Aug-09 00:24:30.064835 32 | # --------------------------------------------------------------------------- +2026-Aug-09 00:24:30.064835 -------------------- +2026-Aug-09 00:24:30.064835 ERROR: failed to build: failed to solve: process "/bin/sh -c npm ci --no-audit --no-fund" did not complete successfully: exit code: 1 +2026-Aug-09 00:24:30.064835 exit status 1 +2026-Aug-09 00:24:30.088572 [DEBUG] Error type: App\Exceptions\DeploymentException +2026-Aug-09 00:24:30.108446 [DEBUG] Error code: 0 +2026-Aug-09 00:24:30.131136 [DEBUG] Location: /var/www/html/app/Traits/ExecuteRemoteCommand.php:242 +2026-Aug-09 00:24:30.152173 [DEBUG] Stack trace (first 5 lines): +2026-Aug-09 00:24:30.176971 [DEBUG] #0 /var/www/html/app/Traits/ExecuteRemoteCommand.php(106): App\Jobs\ApplicationDeploymentJob->executeCommandWithProcess() +2026-Aug-09 00:24:30.201045 [DEBUG] #1 /var/www/html/vendor/laravel/framework/src/Illuminate/Collections/Traits/EnumeratesValues.php(275): App\Jobs\ApplicationDeploymentJob->{closure:App\Traits\ExecuteRemoteCommand::execute_remote_command():72}() +2026-Aug-09 00:24:30.226193 [DEBUG] #2 /var/www/html/app/Traits/ExecuteRemoteCommand.php(72): Illuminate\Support\Collection->each() +2026-Aug-09 00:24:30.245142 [DEBUG] #3 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(3821): App\Jobs\ApplicationDeploymentJob->execute_remote_command() +2026-Aug-09 00:24:30.265233 [DEBUG] #4 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(912): App\Jobs\ApplicationDeploymentJob->build_image() +2026-Aug-09 00:24:30.284294 ======================================== +2026-Aug-09 00:24:30.308183 Deployment failed. Removing the new version of your application. +2026-Aug-09 00:24:31.172542 Gracefully shutting down build container: hyq204tdb0sbkisvotvcoxjk +2026-Aug-09 00:24:31.888162 [DEBUG] [CMD]: docker stop --time=30 hyq204tdb0sbkisvotvcoxjk +2026-Aug-09 00:24:31.888162 [DEBUG] hyq204tdb0sbkisvotvcoxjk \ No newline at end of file diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/9415c3a2-192f-41f4-8bd5-282c65bf987f-deployment-ddwhqdpyhz5ypb0ahlsx06z8-all-logs-2026-08-09-18-33-21.txt b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/9415c3a2-192f-41f4-8bd5-282c65bf987f-deployment-ddwhqdpyhz5ypb0ahlsx06z8-all-logs-2026-08-09-18-33-21.txt new file mode 100644 index 0000000..559925e --- /dev/null +++ b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/9415c3a2-192f-41f4-8bd5-282c65bf987f-deployment-ddwhqdpyhz5ypb0ahlsx06z8-all-logs-2026-08-09-18-33-21.txt @@ -0,0 +1,104 @@ +2026-Aug-09 18:30:56.374118 Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost). +2026-Aug-09 18:30:56.396461 Starting deployment of http://forgejo-b82b4lpyyzyg40thdsjdd38i.163.245.214.68.sslip.io/David/MEGAINFRA-WEBAPP.git:@\(.*\):#https://\1/#g" '\''/artifacts/ddwhqdpyhz5ypb0ahlsx06z8'\''/.gitmodules || true && git -c http.version=HTTP/1.1 submodule sync && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 submodule update --init --recursive --depth=1; fi && cd '\''/artifacts/ddwhqdpyhz5ypb0ahlsx06z8'\'' && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 lfs pull' +2026-Aug-09 18:@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 18:31:24.636144 [DEBUG] #3 CACHED +2026-Aug-09 18:31:24.925384 [DEBUG] #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 18:31:26.396240 [DEBUG] #4 DONE 1.3s +2026-Aug-09 18:31:26.396240 [DEBUG] +2026-Aug-09 18:31:26.396240 [DEBUG] #5 [internal] load .dockerignore +2026-Aug-09 18:31:26.396240 [DEBUG] #5 transferring context: +2026-Aug-09 18:31:26.607741 [DEBUG] #5 transferring context: 764B 0.0s done +2026-Aug-09 18:31:26.607741 [DEBUG] #5 DONE 0.2s +2026-Aug-09 18:31:26.650259 [DEBUG] #6 [runner 2/6] WORKDIR /app +2026-Aug-09 18:31:26.650259 [DEBUG] #6 CACHED +2026-Aug-09 18:31:26.650259 [DEBUG] +2026-Aug-09 18:31:26.650259 [DEBUG] #7 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 18:31:26.650259 [DEBUG] #7 CACHED +2026-Aug-09 18:31:26.758801 [DEBUG] #8 [internal] load build context +2026-Aug-09 18:31:27.027641 [DEBUG] #8 transferring context: 969.58kB 0.2s done +2026-Aug-09 18:31:27.226125 [DEBUG] #8 DONE 0.4s +2026-Aug-09 18:31:27.226125 [DEBUG] +2026-Aug-09 18:31:27.226125 [DEBUG] #9 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 18:31:27.336687 [DEBUG] #9 DONE 0.6s +2026-Aug-09 18:31:27.336687 [DEBUG] +2026-Aug-09 18:31:27.336687 [DEBUG] #10 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 18:31:33.800673 [DEBUG] #10 7.070 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 18:31:33.919334 [DEBUG] #10 7.238 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 18:31:34.092642 [DEBUG] #10 7.408 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 18:31:34.300133 [DEBUG] #10 7.618 OK: 11.0 MiB in 21 packages +2026-Aug-09 18:31:34.466745 [DEBUG] #10 DONE 7.8s +2026-Aug-09 18:31:34.605571 [DEBUG] #11 [deps 3/5] WORKDIR /app +2026-Aug-09 18:31:34.605571 [DEBUG] #11 DONE 0.1s +2026-Aug-09 18:31:34.773809 [DEBUG] #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 18:31:34.809310 [DEBUG] #12 DONE 0.2s +2026-Aug-09 18:31:34.977430 [DEBUG] #13 [deps 5/5] RUN npm ci --ignore-scripts --no-audit --no-fund +2026-Aug-09 18:32:15.587184 [DEBUG] #13 40.76 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 18:32:26.239527 [DEBUG] #13 51.42 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 18:32:26.358184 [DEBUG] #13 51.54 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 18:32:37.716653 ======================================== +2026-Aug-09 18:32:37.763836 Deployment failed: Command execution failed (exit code 255): docker exec ddwhqdpyhz5ypb0ahlsx06z8 bash -c 'bash /artifacts/build.sh' +2026-Aug-09 18:32:37.763836 Error: #0 building with "default" instance using docker driver +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #1 [internal] load build definition from Dockerfile +2026-Aug-09 18:32:37.763836 #1 transferring dockerfile: +2026-Aug-09 18:32:37.763836 #1 transferring dockerfile: 4.02kB 0.1s done +2026-Aug-09 18:32:37.763836 #1 DONE 0.3s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #2 resolve image config for docker-image://docker.io/docker/dockerfile:1.7 +2026-Aug-09 18:32:37.763836 #2 DONE 1.9s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #3 docker-image://docker.io/docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 18:32:37.763836 #3 CACHED +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 18:32:37.763836 #4 DONE 1.3s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #5 [internal] load .dockerignore +2026-Aug-09 18:32:37.763836 #5 transferring context: +2026-Aug-09 18:32:37.763836 #5 transferring context: 764B 0.0s done +2026-Aug-09 18:32:37.763836 #5 DONE 0.2s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #6 [runner 2/6] WORKDIR /app +2026-Aug-09 18:32:37.763836 #6 CACHED +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #7 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 18:32:37.763836 #7 CACHED +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #8 [internal] load build context +2026-Aug-09 18:32:37.763836 #8 transferring context: 969.58kB 0.2s done +2026-Aug-09 18:32:37.763836 #8 DONE 0.4s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #9 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 18:32:37.763836 #9 DONE 0.6s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #10 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 18:32:37.763836 #10 7.070 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 18:32:37.763836 #10 7.238 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 18:32:37.763836 #10 7.408 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 18:32:37.763836 #10 7.618 OK: 11.0 MiB in 21 packages +2026-Aug-09 18:32:37.763836 #10 DONE 7.8s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #11 [deps 3/5] WORKDIR /app +2026-Aug-09 18:32:37.763836 #11 DONE 0.1s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 18:32:37.763836 #12 DONE 0.2s +2026-Aug-09 18:32:37.763836 +2026-Aug-09 18:32:37.763836 #13 [deps 5/5] RUN npm ci --ignore-scripts --no-audit --no-fund +2026-Aug-09 18:32:37.763836 #13 40.76 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 18:32:37.763836 #13 51.42 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 18:32:37.763836 #13 51.54 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 18:32:37.799350 [DEBUG] Error type: App\Exceptions\DeploymentException +2026-Aug-09 18:32:37.846153 [DEBUG] Error code: 0 +2026-Aug-09 18:32:37.909961 [DEBUG] Location: /var/www/html/app/Traits/ExecuteRemoteCommand.php:242 +2026-Aug-09 18:32:37.934826 [DEBUG] Stack trace (first 5 lines): +2026-Aug-09 18:32:37.978692 [DEBUG] #0 /var/www/html/app/Traits/ExecuteRemoteCommand.php(106): App\Jobs\ApplicationDeploymentJob->executeCommandWithProcess() +2026-Aug-09 18:32:38.016770 [DEBUG] #1 /var/www/html/vendor/laravel/framework/src/Illuminate/Collections/Traits/EnumeratesValues.php(275): App\Jobs\ApplicationDeploymentJob->{closure:App\Traits\ExecuteRemoteCommand::execute_remote_command():72}() +2026-Aug-09 18:32:38.055046 [DEBUG] #2 /var/www/html/app/Traits/ExecuteRemoteCommand.php(72): Illuminate\Support\Collection->each() +2026-Aug-09 18:32:38.091398 [DEBUG] #3 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(3821): App\Jobs\ApplicationDeploymentJob->execute_remote_command() +2026-Aug-09 18:32:38.132071 [DEBUG] #4 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(912): App\Jobs\ApplicationDeploymentJob->build_image() +2026-Aug-09 18:32:38.157232 ======================================== +2026-Aug-09 18:32:38.187298 Deployment failed. Removing the new version of your application. +2026-Aug-09 18:32:39.374733 Gracefully shutting down build container: ddwhqdpyhz5ypb0ahlsx06z8 +2026-Aug-09 18:32:41.667553 [DEBUG] [CMD]: docker stop --time=30 ddwhqdpyhz5ypb0ahlsx06z8 +2026-Aug-09 18:32:41.667553 [DEBUG] ddwhqdpyhz5ypb0ahlsx06z8 \ No newline at end of file diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/bd8ee788-6a4f-4602-9153-143c1b5d6558-AI.xlsx b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/bd8ee788-6a4f-4602-9153-143c1b5d6558-AI.xlsx new file mode 100644 index 0000000..6171beb Binary files /dev/null and b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/bd8ee788-6a4f-4602-9153-143c1b5d6558-AI.xlsx differ diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/c9242d7c-0d62-401d-8f72-06c841c3607b-deployment-qkvx1xuygl2okqnv67v1020c-all-logs-2026-08-09-18-28-31.txt b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/c9242d7c-0d62-401d-8f72-06c841c3607b-deployment-qkvx1xuygl2okqnv67v1020c-all-logs-2026-08-09-18-28-31.txt new file mode 100644 index 0000000..347c651 --- /dev/null +++ b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/c9242d7c-0d62-401d-8f72-06c841c3607b-deployment-qkvx1xuygl2okqnv67v1020c-all-logs-2026-08-09-18-28-31.txt @@ -0,0 +1,226 @@ +2026-Aug-09 18:25:11.686798 Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost). +2026-Aug-09 18:25:11.705078 Starting deployment of http://forgejo-b82b4lpyyzyg40thdsjdd38i.163.245.214.68.sslip.io/David/MEGAINFRA-WEBAPP.git:@\(.*\):#https://\1/#g" '\''/artifacts/qkvx1xuygl2okqnv67v1020c'\''/.gitmodules || true && git -c http.version=HTTP/1.1 submodule sync && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 submodule update --init --recursive --depth=1; fi && cd '\''/artifacts/qkvx1xuygl2okqnv67v1020c'\'' && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git -c http.version=HTTP/1.1 lfs pull' +2026-Aug-09 18:@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 18:25:26.173454 [DEBUG] #3 CACHED +2026-Aug-09 18:25:26.314695 [DEBUG] #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 18:25:27.915533 [DEBUG] #4 DONE 1.6s +2026-Aug-09 18:25:28.078519 [DEBUG] #5 [internal] load .dockerignore +2026-Aug-09 18:25:28.078519 [DEBUG] #5 transferring context: 780B done +2026-Aug-09 18:25:28.078519 [DEBUG] #5 DONE 0.0s +2026-Aug-09 18:25:28.220347 [DEBUG] #6 [runner 2/6] WORKDIR /app +2026-Aug-09 18:25:28.220347 [DEBUG] #6 CACHED +2026-Aug-09 18:25:28.220347 [DEBUG] +2026-Aug-09 18:25:28.220347 [DEBUG] #7 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 18:25:28.220347 [DEBUG] #7 CACHED +2026-Aug-09 18:25:28.220347 [DEBUG] +2026-Aug-09 18:25:28.220347 [DEBUG] #8 [internal] load build context +2026-Aug-09 18:25:28.390813 [DEBUG] #8 transferring context: 961.23kB 0.1s done +2026-Aug-09 18:25:28.390813 [DEBUG] #8 DONE 0.2s +2026-Aug-09 18:25:28.390813 [DEBUG] +2026-Aug-09 18:25:28.390813 [DEBUG] #9 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 18:25:28.390813 [DEBUG] #9 DONE 0.3s +2026-Aug-09 18:25:28.390813 [DEBUG] +2026-Aug-09 18:25:28.390813 [DEBUG] #10 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 18:25:48.625054 [DEBUG] #10 20.54 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 18:25:48.814112 [DEBUG] #10 20.73 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 18:25:48.997554 [DEBUG] #10 20.92 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 18:25:49.428313 [DEBUG] #10 21.33 OK: 11.0 MiB in 21 packages +2026-Aug-09 18:25:49.553636 [DEBUG] #10 DONE 21.5s +2026-Aug-09 18:25:49.703504 [DEBUG] +2026-Aug-09 18:25:49.710468 [DEBUG] #11 [deps 3/5] WORKDIR /app +2026-Aug-09 18:25:49.710468 [DEBUG] #11 DONE 0.1s +2026-Aug-09 18:25:49.710468 [DEBUG] +2026-Aug-09 18:25:49.710468 [DEBUG] #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 18:25:49.710468 [DEBUG] #12 DONE 0.1s +2026-Aug-09 18:25:49.857306 [DEBUG] #13 [deps 5/5] RUN npm ci --ignore-scripts --no-audit --no-fund +2026-Aug-09 18:25:59.507619 [DEBUG] #13 9.800 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 18:26:03.447281 [DEBUG] #13 13.73 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 18:26:03.691330 [DEBUG] #13 13.97 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 18:26:22.858528 [DEBUG] #13 33.15 npm warn deprecated recharts@2.15.4: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:26:48.093519 [DEBUG] #13 58.23 npm notice +2026-Aug-09 18:26:49.296198 [DEBUG] #13 DONE 59.6s +2026-Aug-09 18:27:36.190888 [DEBUG] #14 [builder 3/5] COPY --from=deps /app/node_modules ./node_modules +2026-Aug-09 18:28:20.456063 [DEBUG] #14 DONE 44.3s +2026-Aug-09 18:28:20.610735 [DEBUG] #15 [builder 4/5] COPY . . +2026-Aug-09 18:28:20.660383 [DEBUG] #15 DONE 0.2s +2026-Aug-09 18:28:20.816605 [DEBUG] #16 [builder 5/5] RUN npm run build +2026-Aug-09 18:28:22.946942 [DEBUG] #16 2.283 +2026-Aug-09 18:28:22.946942 [DEBUG] #16 2.283 > mega-infrastructure-dashboard@1.0.0 build +2026-Aug-09 18:28:22.946942 [DEBUG] #16 2.283 > npm run og-image && next build --webpack +2026-Aug-09 18:28:22.946942 [DEBUG] #16 2.283 +2026-Aug-09 18:28:23.473138 [DEBUG] #16 2.807 +2026-Aug-09 18:28:23.473138 [DEBUG] #16 2.807 > mega-infrastructure-dashboard@1.0.0 og-image +2026-Aug-09 18:28:23.473138 [DEBUG] #16 2.807 > node scripts/build-og-image.mjs +2026-Aug-09 18:28:23.473138 [DEBUG] #16 2.807 +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 node:internal/modules/cjs/loader:1210 +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 throw err; +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 ^ +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 Error: Cannot find module '/app/scripts/build-og-image.mjs' +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15) +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 at Module._load (node:internal/modules/cjs/loader:1038:27) +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 at node:internal/main/run_main_module:28:49 { +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 code: 'MODULE_NOT_FOUND', +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 requireStack: [] +2026-Aug-09 18:28:23.676595 [DEBUG] #16 3.009 } +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.017 +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.017 Node.js v20.20.2 +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.067 npm notice +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.067 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.067 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:28:23.860081 [DEBUG] #16 3.067 npm notice +2026-Aug-09 18:28:23.860081 [DEBUG] #16 ERROR: process "/bin/sh -c npm run build" did not complete successfully: exit code: 1 +2026-Aug-09 18:28:24.006330 [DEBUG] ------ +2026-Aug-09 18:28:24.006330 [DEBUG] > [builder 5/5] RUN npm run build: +2026-Aug-09 18:28:24.006330 [DEBUG] 3.009 code: 'MODULE_NOT_FOUND', +2026-Aug-09 18:28:24.006330 [DEBUG] 3.009 requireStack: [] +2026-Aug-09 18:28:24.006330 [DEBUG] 3.009 } +2026-Aug-09 18:28:24.006330 [DEBUG] 3.017 +2026-Aug-09 18:28:24.006330 [DEBUG] 3.017 Node.js v20.20.2 +2026-Aug-09 18:28:24.006330 [DEBUG] 3.067 npm notice +2026-Aug-09 18:28:24.006330 [DEBUG] 3.067 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 18:28:24.006330 [DEBUG] 3.067 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:28:24.006330 [DEBUG] 3.067 npm notice +2026-Aug-09 18:28:24.006330 [DEBUG] ------ +2026-Aug-09 18:28:24.026217 [DEBUG] Dockerfile:62 +2026-Aug-09 18:28:24.026217 [DEBUG] -------------------- +2026-Aug-09 18:28:24.026217 [DEBUG] 60 | # already contains the trimmed `node_modules` needed at runtime plus a +2026-Aug-09 18:28:24.026217 [DEBUG] 61 | # ready-to-run `server.js`. +2026-Aug-09 18:28:24.026217 [DEBUG] 62 | >>> RUN npm run build +2026-Aug-09 18:28:24.026217 [DEBUG] 63 | +2026-Aug-09 18:28:24.026217 [DEBUG] 64 | # --------------------------------------------------------------------------- +2026-Aug-09 18:28:24.026217 [DEBUG] -------------------- +2026-Aug-09 18:28:24.026217 [DEBUG] ERROR: failed to build: failed to solve: process "/bin/sh -c npm run build" did not complete successfully: exit code: 1 +2026-Aug-09 18:28:24.051956 [DEBUG] exit status 1 +2026-Aug-09 18:28:24.231858 ======================================== +2026-Aug-09 18:28:24.249189 Deployment failed: Command execution failed (exit code 1): docker exec qkvx1xuygl2okqnv67v1020c bash -c 'bash /artifacts/build.sh' +2026-Aug-09 18:28:24.249189 Error: #0 building with "default" instance using docker driver +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #1 [internal] load build definition from Dockerfile +2026-Aug-09 18:28:24.249189 #1 transferring dockerfile: 4.02kB done +2026-Aug-09 18:28:24.249189 #1 DONE 0.1s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #2 resolve image config for docker-image://docker.io/docker/dockerfile:1.7 +2026-Aug-09 18:28:24.249189 #2 DONE 1.8s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #3 docker-image://docker.io/docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e +2026-Aug-09 18:28:24.249189 #3 CACHED +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #4 [internal] load metadata for docker.io/library/node:20-alpine +2026-Aug-09 18:28:24.249189 #4 DONE 1.6s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #5 [internal] load .dockerignore +2026-Aug-09 18:28:24.249189 #5 transferring context: 780B done +2026-Aug-09 18:28:24.249189 #5 DONE 0.0s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #6 [runner 2/6] WORKDIR /app +2026-Aug-09 18:28:24.249189 #6 CACHED +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #7 [runner 1/6] FROM docker.io/library/node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 +2026-Aug-09 18:28:24.249189 #7 CACHED +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #8 [internal] load build context +2026-Aug-09 18:28:24.249189 #8 transferring context: 961.23kB 0.1s done +2026-Aug-09 18:28:24.249189 #8 DONE 0.2s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #9 [runner 3/6] RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +2026-Aug-09 18:28:24.249189 #9 DONE 0.3s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #10 [deps 2/5] RUN apk add --no-cache libc6-compat +2026-Aug-09 18:28:24.249189 #10 20.54 (1/3) Installing musl-obstack (1.2.3-r2) +2026-Aug-09 18:28:24.249189 #10 20.73 (2/3) Installing libucontext (1.3.3-r0) +2026-Aug-09 18:28:24.249189 #10 20.92 (3/3) Installing gcompat (1.1.0-r4) +2026-Aug-09 18:28:24.249189 #10 21.33 OK: 11.0 MiB in 21 packages +2026-Aug-09 18:28:24.249189 #10 DONE 21.5s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #11 [deps 3/5] WORKDIR /app +2026-Aug-09 18:28:24.249189 #11 DONE 0.1s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #12 [deps 4/5] COPY package.json package-lock.json* ./ +2026-Aug-09 18:28:24.249189 #12 DONE 0.1s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #13 [deps 5/5] RUN npm ci --ignore-scripts --no-audit --no-fund +2026-Aug-09 18:28:24.249189 #13 9.800 npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead +2026-Aug-09 18:28:24.249189 #13 13.73 npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +2026-Aug-09 18:28:24.249189 #13 13.97 npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting +2026-Aug-09 18:28:24.249189 #13 33.15 npm warn deprecated recharts@2.15.4: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:28:24.249189 #13 58.23 npm notice +2026-Aug-09 18:28:24.249189 #13 DONE 59.6s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #14 [builder 3/5] COPY --from=deps /app/node_modules ./node_modules +2026-Aug-09 18:28:24.249189 #14 DONE 44.3s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #15 [builder 4/5] COPY . . +2026-Aug-09 18:28:24.249189 #15 DONE 0.2s +2026-Aug-09 18:28:24.249189 +2026-Aug-09 18:28:24.249189 #16 [builder 5/5] RUN npm run build +2026-Aug-09 18:28:24.249189 #16 2.283 +2026-Aug-09 18:28:24.249189 #16 2.283 > mega-infrastructure-dashboard@1.0.0 build +2026-Aug-09 18:28:24.249189 #16 2.283 > npm run og-image && next build --webpack +2026-Aug-09 18:28:24.249189 #16 2.283 +2026-Aug-09 18:28:24.249189 #16 2.807 +2026-Aug-09 18:28:24.249189 #16 2.807 > mega-infrastructure-dashboard@1.0.0 og-image +2026-Aug-09 18:28:24.249189 #16 2.807 > node scripts/build-og-image.mjs +2026-Aug-09 18:28:24.249189 #16 2.807 +2026-Aug-09 18:28:24.249189 #16 3.009 node:internal/modules/cjs/loader:1210 +2026-Aug-09 18:28:24.249189 #16 3.009 throw err; +2026-Aug-09 18:28:24.249189 #16 3.009 ^ +2026-Aug-09 18:28:24.249189 #16 3.009 +2026-Aug-09 18:28:24.249189 #16 3.009 Error: Cannot find module '/app/scripts/build-og-image.mjs' +2026-Aug-09 18:28:24.249189 #16 3.009 at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15) +2026-Aug-09 18:28:24.249189 #16 3.009 at Module._load (node:internal/modules/cjs/loader:1038:27) +2026-Aug-09 18:28:24.249189 #16 3.009 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) +2026-Aug-09 18:28:24.249189 #16 3.009 at node:internal/main/run_main_module:28:49 { +2026-Aug-09 18:28:24.249189 #16 3.009 code: 'MODULE_NOT_FOUND', +2026-Aug-09 18:28:24.249189 #16 3.009 requireStack: [] +2026-Aug-09 18:28:24.249189 #16 3.009 } +2026-Aug-09 18:28:24.249189 #16 3.017 +2026-Aug-09 18:28:24.249189 #16 3.017 Node.js v20.20.2 +2026-Aug-09 18:28:24.249189 #16 3.067 npm notice +2026-Aug-09 18:28:24.249189 #16 3.067 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 18:28:24.249189 #16 3.067 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:28:24.249189 #16 3.067 npm notice +2026-Aug-09 18:28:24.249189 #16 ERROR: process "/bin/sh -c npm run build" did not complete successfully: exit code: 1 +2026-Aug-09 18:28:24.249189 ------ +2026-Aug-09 18:28:24.249189 > [builder 5/5] RUN npm run build: +2026-Aug-09 18:28:24.249189 3.009 code: 'MODULE_NOT_FOUND', +2026-Aug-09 18:28:24.249189 3.009 requireStack: [] +2026-Aug-09 18:28:24.249189 3.009 } +2026-Aug-09 18:28:24.249189 3.017 +2026-Aug-09 18:28:24.249189 3.017 Node.js v20.20.2 +2026-Aug-09 18:28:24.249189 3.067 npm notice +2026-Aug-09 18:28:24.249189 3.067 npm notice New major version of npm available! 10.8.2 -> 12.0.2 +2026-Aug-09 18:28:24.249189 3.067 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.0.2 +2026-Aug-09 18:@12.0.2 +2026-Aug-09 18:28:24.249189 3.067 npm notice +2026-Aug-09 18:28:24.249189 ------ +2026-Aug-09 18:28:24.249189 Dockerfile:62 +2026-Aug-09 18:28:24.249189 -------------------- +2026-Aug-09 18:28:24.249189 60 | # already contains the trimmed `node_modules` needed at runtime plus a +2026-Aug-09 18:28:24.249189 61 | # ready-to-run `server.js`. +2026-Aug-09 18:28:24.249189 62 | >>> RUN npm run build +2026-Aug-09 18:28:24.249189 63 | +2026-Aug-09 18:28:24.249189 64 | # --------------------------------------------------------------------------- +2026-Aug-09 18:28:24.249189 -------------------- +2026-Aug-09 18:28:24.249189 ERROR: failed to build: failed to solve: process "/bin/sh -c npm run build" did not complete successfully: exit code: 1 +2026-Aug-09 18:28:24.249189 exit status 1 +2026-Aug-09 18:28:24.264866 [DEBUG] Error type: App\Exceptions\DeploymentException +2026-Aug-09 18:28:24.280388 [DEBUG] Error code: 0 +2026-Aug-09 18:28:24.297395 [DEBUG] Location: /var/www/html/app/Traits/ExecuteRemoteCommand.php:242 +2026-Aug-09 18:28:24.313067 [DEBUG] Stack trace (first 5 lines): +2026-Aug-09 18:28:24.328382 [DEBUG] #0 /var/www/html/app/Traits/ExecuteRemoteCommand.php(106): App\Jobs\ApplicationDeploymentJob->executeCommandWithProcess() +2026-Aug-09 18:28:24.347507 [DEBUG] #1 /var/www/html/vendor/laravel/framework/src/Illuminate/Collections/Traits/EnumeratesValues.php(275): App\Jobs\ApplicationDeploymentJob->{closure:App\Traits\ExecuteRemoteCommand::execute_remote_command():72}() +2026-Aug-09 18:28:24.365150 [DEBUG] #2 /var/www/html/app/Traits/ExecuteRemoteCommand.php(72): Illuminate\Support\Collection->each() +2026-Aug-09 18:28:24.382636 [DEBUG] #3 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(3821): App\Jobs\ApplicationDeploymentJob->execute_remote_command() +2026-Aug-09 18:28:24.401357 [DEBUG] #4 /var/www/html/app/Jobs/ApplicationDeploymentJob.php(912): App\Jobs\ApplicationDeploymentJob->build_image() +2026-Aug-09 18:28:24.418246 ======================================== +2026-Aug-09 18:28:24.434314 Deployment failed. Removing the new version of your application. +2026-Aug-09 18:28:25.369820 Gracefully shutting down build container: qkvx1xuygl2okqnv67v1020c +2026-Aug-09 18:28:25.989661 [DEBUG] [CMD]: docker stop --time=30 qkvx1xuygl2okqnv67v1020c +2026-Aug-09 18:28:25.989661 [DEBUG] qkvx1xuygl2okqnv67v1020c \ No newline at end of file diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/e34aa673-990f-4b69-8005-346876474174-image.png b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/e34aa673-990f-4b69-8005-346876474174-image.png new file mode 100644 index 0000000..e0649ff Binary files /dev/null and b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/e34aa673-990f-4b69-8005-346876474174-image.png differ diff --git a/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/f147767e-75cd-43cd-9b3b-af5f11fbf19f-image.png b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/f147767e-75cd-43cd-9b3b-af5f11fbf19f-image.png new file mode 100644 index 0000000..ca7180a Binary files /dev/null and b/.opencode/openwork/inbox/chat-attachments/ses_01cf7df27ffeyy367S81LZdRJG/f147767e-75cd-43cd-9b3b-af5f11fbf19f-image.png differ diff --git a/package-lock.json b/package-lock.json index 197b3aa..1b2a925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "mega-infrastructure-dashboard", "version": "1.0.0", + "hasInstallScript": true, "dependencies": { "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-dialog": "^1.1.4", @@ -23,6 +24,7 @@ "lucide-react": "^0.469.0", "next": "^16", "papaparse": "^5.4.1", + "pocketbase": "^0.27.3", "react": "^19.0.0", "react-dom": "^19.0.0", "recharts": "^2.15.0", @@ -82,7 +84,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1597,7 +1598,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -1609,7 +1609,6 @@ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4082,7 +4081,6 @@ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4093,7 +4091,6 @@ "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4155,7 +4152,6 @@ "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", @@ -4735,6 +4731,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4744,25 +4741,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4773,13 +4774,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4792,6 +4795,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4801,6 +4805,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4809,13 +4814,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4832,6 +4839,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4845,6 +4853,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4857,6 +4866,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4871,6 +4881,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4880,20 +4891,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4933,6 +4945,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4950,6 +4963,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4965,7 +4979,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-styles": { "version": "4.3.0", @@ -5343,7 +5358,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", @@ -5463,6 +5477,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -6079,7 +6094,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.2", @@ -6169,7 +6185,6 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6604,6 +6619,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -7735,6 +7751,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7749,6 +7766,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7765,7 +7783,6 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -8283,7 +8300,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/merge2": { "version": "1.4.1", @@ -8312,6 +8330,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8343,6 +8362,7 @@ "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -8449,14 +8469,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/next/-/next-16.3.0.tgz", "integrity": "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.3.0", "@swc/helpers": "0.5.15", @@ -8839,6 +8859,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pocketbase": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.27.3.tgz", + "integrity": "sha512-n2rtQrg3kTF44066B+2eVvQdiYRGZ+RI7isIZqmAptfElLCSDIZrEKLhSrIQA2k5pvQM29Gx5JqGv5n+gtm6dg==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8954,7 +8980,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8964,7 +8989,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9280,7 +9304,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -9397,6 +9420,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9433,6 +9457,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9444,7 +9469,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.6.3", @@ -10067,7 +10093,6 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10237,7 +10262,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10513,6 +10537,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2" }, @@ -10525,6 +10550,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", @@ -10568,6 +10594,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -10577,6 +10604,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10590,6 +10618,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -10793,7 +10822,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10974,7 +11002,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11100,7 +11127,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 8c70ab7..6843dfa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "next dev -H 0.0.0.0 -p 3000 --webpack", + "dev": "next dev -H 0.0.0.0 -p 6004 --webpack", "build": "npm run og-image && next build --webpack", "og-image": "node scripts/build-og-image.mjs", "start": "next start -H 0.0.0.0 -p 3000", @@ -27,12 +27,14 @@ "lucide-react": "^0.469.0", "next": "^16", "papaparse": "^5.4.1", + "pocketbase": "^0.27.3", "react": "^19.0.0", "react-dom": "^19.0.0", "recharts": "^2.15.0", "tailwind-merge": "^2.6.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", "@types/node": "^22.10.5", "@types/papaparse": "^5.3.15", "@types/react": "^19.0.2", @@ -41,7 +43,6 @@ "eslint-config-next": "^16", "postcss": "^8.4.49", "tailwindcss": "^4.0.0", - "@tailwindcss/postcss": "^4.0.0", "typescript": "^5.7.2" } } diff --git a/scripts/pb-bootstrap-schema.py b/scripts/pb-bootstrap-schema.py new file mode 100644 index 0000000..b4ee123 --- /dev/null +++ b/scripts/pb-bootstrap-schema.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Bootstrap PocketBase schema for MEGA INFRASTRUCTURE. + +PocketBase v0.39 JSON shape: type-specific options (values, maxSelect, +mimeTypes, collectionId, cascadeDelete, min, max) live at the field +top level — NOT nested under "options". +""" +import json +import sys +import urllib.request +import urllib.error + +PB = "https://pb.mega.infra.apphub.co.za" +EMAIL = "admin@mega.co.za" +PASSWORD = "12345678" + + +def http(method, path, body=None, token=None): + url = f"{PB}{path}" + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + if body is not None: + req.add_header("Content-Type", "application/json") + if token: + req.add_header("Authorization", token) + try: + with urllib.request.urlopen(req) as r: + txt = r.read().decode() + return r.status, json.loads(txt) if txt else None + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode() or "{}") + + +code, auth = http("POST", "/api/collections/_superusers/auth-with-password", + {"identity": EMAIL, "password": PASSWORD}) +if code != 200: + sys.exit(f"AUTH FAILED: {code} {auth}") +TOKEN = auth["token"] +print(f"auth ok, superuser id={auth['record']['id']}\n") + +LOCKED = { # superuser-only access + "listRule": None, "viewRule": None, + "createRule": None, "updateRule": None, "deleteRule": None, +} + + +def field(name, type_, **kw): + """Build a field. Extra kwargs (required, values, maxSelect, etc.) + go at the field top level.""" + f = {"name": name, "type": type_} + f.update(kw) + return f + + +DEFS = {} + +DEFS["data_import_batches"] = { + "name": "DATA_IMPORT_BATCHES", "type": "base", + "fields": [ + field("batch_type", "select", required=True, + values=["timesheets", "fuel_logs"], maxSelect=1), + field("original_file", "file", required=False, + maxSelect=1, maxSize=20971520, + mimeTypes=["text/csv", "text/plain", "application/vnd.ms-excel", "application/csv"]), + field("date_uploaded", "date", required=False), + field("uploaded_by", "text", required=False), + ], + "indexes": [], +} + +DEFS["sites"] = { + "name": "SITES", "type": "base", + "fields": [ + field("project_code", "text", required=True, min=1, max=64), + field("name", "text", required=False), + field("duration", "text", required=False), + field("contract_value", "number", required=False), + field("work_hours", "number", required=False), + field("hours_per_week", "number", required=False), + field("wages_hours_per_day", "number", required=False), + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_sites_project_code` ON `sites` (`project_code`)", + ], +} + +DEFS["employees"] = { + "name": "EMPLOYEES", "type": "base", + "fields": [ + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("name", "text", required=False), + field("employee_no", "text", required=False), + field("position", "text", required=False), + field("rate_per_hour", "number", required=False), + field("hours_per_day_standard", "number", required=False), + field("worker_type", "select", required=False, + values=["permanent", "casual"], maxSelect=1), + ], + "indexes": [], +} + +DEFS["purchase_orders"] = { + "name": "PURCHASE_ORDERS", "type": "base", + "fields": [ + field("purchase_order_number", "text", required=True, min=1, max=128), + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("supplier", "text", required=False), + field("required_by_date", "date", required=False), + field("ordered_by", "text", required=False), + field("items_count", "number", required=False), + field("total_value", "number", required=False), + field("purchase_order_file", "file", required=False, + maxSelect=1, maxSize=20971520, + mimeTypes=["application/pdf", "image/jpeg", "image/png", "image/webp"]), + field("raw_ocr_data", "json", required=False), + field("is_verified", "bool", required=False), + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_po_number` ON `purchase_orders` (`purchase_order_number`)", + ], +} + +DEFS["purchase_order_items"] = { + "name": "PURCHASE_ORDER_ITEMS", "type": "base", + "fields": [ + field("purchase_order", "relation", required=False, + collectionId="__PURCHASE_ORDERS_ID__", cascadeDelete=True, maxSelect=1), + field("material_code", "text", required=False), + field("cost_code", "text", required=False), + field("description", "text", required=False), + field("qty", "number", required=False), + field("qty_outstanding", "number", required=False), + field("unit", "text", required=False), + field("rate", "number", required=False), + field("disc_percent", "number", required=False), + field("vat_percent", "number", required=False), + field("amount", "number", required=False), + ], + "indexes": [], +} + +DEFS["fixed_costs"] = { + "name": "FIXED_COSTS", "type": "base", + "fields": [ + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("date_logged", "date", required=False), + field("description", "text", required=False), + field("cost", "number", required=False), + field("notes", "text", required=False), + ], + "indexes": [], +} + +DEFS["plant"] = { + "name": "PLANT", "type": "base", + "fields": [ + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("description", "text", required=False), + field("standard_work_hours", "text", required=False), + field("cost_per_hour", "number", required=False), + ], + "indexes": [], +} + +DEFS["vehicles"] = { + "name": "VEHICLES", "type": "base", + "fields": [ + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("description", "text", required=False), + field("standard_work_hours", "text", required=False), + field("fuel_limits", "text", required=False), + ], + "indexes": [], +} + +DEFS["timesheets"] = { + "name": "TIMESHEETS", "type": "base", + "fields": [ + field("employee", "relation", required=False, + collectionId="__EMPLOYEES_ID__", cascadeDelete=False, maxSelect=1), + field("batch_reference", "relation", required=False, + collectionId="__DATA_IMPORT_BATCHES_ID__", cascadeDelete=False, maxSelect=1), + field("date_logged", "date", required=False), + field("hours_worked", "number", required=False), + ], + "indexes": [], +} + +DEFS["fuel_logs"] = { + "name": "FUEL_LOGS", "type": "base", + "fields": [ + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("vehicle", "relation", required=False, + collectionId="__VEHICLES_ID__", cascadeDelete=False, maxSelect=1), + field("batch_reference", "relation", required=False, + collectionId="__DATA_IMPORT_BATCHES_ID__", cascadeDelete=False, maxSelect=1), + field("date_logged", "date", required=False), + field("litres", "number", required=False), + field("odo_reading", "number", required=False), + ], + "indexes": [], +} + +DEFS["delivery_notes"] = { + "name": "DELIVERY_NOTES", "type": "base", + "fields": [ + field("delivery_note_number", "text", required=False), + field("project_code", "text", required=False), + field("site", "relation", required=False, + collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1), + field("purchase_order", "relation", required=False, + collectionId="__PURCHASE_ORDERS_ID__", cascadeDelete=False, maxSelect=1), + field("date_received", "date", required=False), + field("received_by", "text", required=False), + field("slip_image", "file", required=False, + maxSelect=1, maxSize=20971520, + mimeTypes=["image/jpeg", "image/png", "image/webp", "application/pdf"]), + field("raw_ocr_data", "json", required=False), + field("is_verified", "bool", required=False), + ], + "indexes": [], +} + +DEFS["received_items"] = { + "name": "RECEIVED_ITEMS", "type": "base", + "fields": [ + field("delivery_note", "relation", required=False, + collectionId="__DELIVERY_NOTES_ID__", cascadeDelete=True, maxSelect=1), + field("purchase_order_item", "relation", required=False, + collectionId="__PURCHASE_ORDER_ITEMS_ID__", cascadeDelete=False, maxSelect=1), + field("qty_received", "number", required=False), + field("condition_notes", "text", required=False), + ], + "indexes": [], +} + +ORDER = [ + "data_import_batches", + "sites", + "employees", + "purchase_orders", + "purchase_order_items", + "fixed_costs", + "plant", + "vehicles", + "timesheets", + "fuel_logs", + "delivery_notes", + "received_items", +] + +ids = {} +for key in ORDER: + body = dict(DEFS[key]) + body.update(LOCKED) + for f in body["fields"]: + if f.get("type") == "relation": + cid = f.get("collectionId", "") + # placeholders use collection table name in upper, e.g. __SITES_ID__ + for other_key, other_id in ids.items(): + collection_name_upper = DEFS[other_key]["name"] # e.g. "SITES" + cid = cid.replace(f"__{collection_name_upper}_ID__", other_id) + if "__" in cid: + sys.exit(f"unresolved placeholder in {key}.{f['name']}: {cid}") + f["collectionId"] = cid + + code, resp = http("POST", "/api/collections", body, TOKEN) + if code == 200: + ids[key] = resp["id"] + print(f" ✓ {body['name']:25s} → {resp['id']}") + elif code == 400: + code2, existing = http("GET", f"/api/collections/{body['name']}", token=TOKEN) + if code2 == 200: + ids[key] = existing["id"] + print(f" • {body['name']:25s} → {existing['id']} (already existed)") + else: + print(f" ✗ {body['name']:25s} FAILED: {resp}", file=sys.stderr) + sys.exit(1) + else: + print(f" ✗ {body['name']:25s} FAILED: {code} {resp}", file=sys.stderr) + sys.exit(1) + +print(f"\nDone. {len(ids)} collections on {PB}.") diff --git a/scripts/pb-relax-rules.py b/scripts/pb-relax-rules.py new file mode 100644 index 0000000..8282fe0 --- /dev/null +++ b/scripts/pb-relax-rules.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Relax API rules on the 12 MEGA INFRASTRUCTURE collections. + +Pattern: + list/view/create → any authenticated user + update/delete → superuser only (audit-safe for OCR-verified records) + +Override per-collection with the RULES dict if you need different behaviour. +""" +import json +import sys +import urllib.request +import urllib.error + +PB = "https://pb.mega.infra.apphub.co.za" +EMAIL = "admin@mega.co.za" +PASSWORD = "12345678" + +AUTH_RULE = "@request.auth.id != ''" # any logged-in user +NONE_RULE = None # superuser only + +RULES = { + "DATA_IMPORT_BATCHES": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "SITES": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "EMPLOYEES": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "PURCHASE_ORDERS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "PURCHASE_ORDER_ITEMS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "FIXED_COSTS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "PLANT": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "VEHICLES": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "TIMESHEETS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "FUEL_LOGS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "DELIVERY_NOTES": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), + "RECEIVED_ITEMS": (AUTH_RULE, AUTH_RULE, AUTH_RULE, NONE_RULE, NONE_RULE), +} + + +def http(method, path, body=None, token=None): + url = f"{PB}{path}" + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + if body is not None: + req.add_header("Content-Type", "application/json") + if token: + req.add_header("Authorization", token) + try: + with urllib.request.urlopen(req) as r: + txt = r.read().decode() + return r.status, json.loads(txt) if txt else None + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode() or "{}") + + +code, auth = http("POST", "/api/collections/_superusers/auth-with-password", + {"identity": EMAIL, "password": PASSWORD}) +if code != 200: + sys.exit(f"AUTH FAILED: {code} {auth}") +TOKEN = auth["token"] + +# fetch all collections to map name → id +code, listing = http("GET", "/api/collections?perPage=100", token=TOKEN) +if code != 200: + sys.exit(f"could not list collections: {code}") +by_name = {c["name"]: c["id"] for c in listing["items"]} + +for name, (l, v, c, u, d) in RULES.items(): + cid = by_name.get(name) + if not cid: + print(f" ✗ {name:25s} not found, skipping") + continue + code, resp = http("PATCH", f"/api/collections/{cid}", + {"listRule": l, "viewRule": v, "createRule": c, + "updateRule": u, "deleteRule": d}, TOKEN) + if code == 200: + print(f" ✓ {name:25s} updated") + else: + print(f" ✗ {name:25s} FAILED: {code} {resp}") + +print("\nDone.") diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts deleted file mode 100644 index 7420657..0000000 --- a/src/app/api/health/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextResponse } from "next/server"; -import { EMPLOYEES, FUEL_READINGS, ORDERS, VEHICLES } from "@/lib/mock-data"; - -export async function GET() { - return NextResponse.json({ - company: "MEGA INFRASTRUCTURE", - counts: { - vehicles: VEHICLES.length, - fuelReadings: FUEL_READINGS.length, - orders: ORDERS.length, - employees: EMPLOYEES.length, - }, - sites: Array.from(new Set(VEHICLES.map((v) => v.assignedSite))), - modules: ["/dashboard", "/dashboard/fuel", "/dashboard/orders", "/dashboard/employees"], - reporting: "/api/mcp?module=all|fuel|orders|employees", - uploads: "/api/upload", - }); -} diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts deleted file mode 100644 index d70d131..0000000 --- a/src/app/api/mcp/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from "next/server"; -import { EMPLOYEES, FUEL_READINGS, ORDERS, VEHICLES } from "@/lib/mock-data"; -import { analyseFuel } from "@/lib/fuel"; - -/** - * MCP-style reporting endpoint for the AI agent. - * - * Returns the same data shape the dashboard uses, in a compact JSON form so - * an external agent (e.g. an MCP server, a Make / n8n automation, or the - * site manager's copilot) can answer questions like: - * "how many fuel anomalies this week?" - * "which orders are overdue?" - * "who is overworked?" - */ -export async function GET(request: Request) { - const url = new URL(request.url); - const module = url.searchParams.get("module") ?? "all"; - const anomalies = analyseFuel(FUEL_READINGS, VEHICLES); - const flagged = anomalies.filter((a) => a.severity !== "ok"); - - const payload: Record = {}; - - if (module === "all" || module === "fuel") { - payload.fuel = { - readingsCount: FUEL_READINGS.length, - vehiclesCount: VEHICLES.length, - anomaliesCount: flagged.length, - critical: flagged.filter((a) => a.severity === "critical").length, - alert: flagged.filter((a) => a.severity === "alert").length, - watch: flagged.filter((a) => a.severity === "watch").length, - topAnomalies: flagged.slice(0, 5), - }; - } - - if (module === "all" || module === "orders") { - const overdue = ORDERS.filter( - (o) => o.status !== "Delivered" && o.status !== "Cancelled" && new Date(o.expectedAt) < new Date() - ); - payload.orders = { - total: ORDERS.length, - open: ORDERS.filter((o) => o.status !== "Delivered" && o.status !== "Cancelled").length, - overdue: overdue.length, - totalValue: ORDERS.reduce((s, o) => s + o.totalValue, 0), - outstandingLines: ORDERS.reduce( - (s, o) => s + o.lines.filter((l) => l.quantityDelivered < l.quantityOrdered).length, - 0 - ), - byStatus: Array.from( - ORDERS.reduce((m, o) => m.set(o.status, (m.get(o.status) ?? 0) + 1), new Map()) - ).map(([status, count]) => ({ status, count })), - }; - } - - if (module === "all" || module === "employees") { - const overworked = EMPLOYEES.filter((e) => e.weeklyOvertimeHours > 6); - const onLeave = EMPLOYEES.filter((e) => e.status === "On Leave"); - const absent = EMPLOYEES.filter( - (e) => e.dailyHours.slice(1).every((d) => d.hours === 0) && e.status === "Active" - ); - payload.employees = { - total: EMPLOYEES.length, - onLeave: onLeave.length, - absent: absent.length, - overworked: overworked.length, - weeklyHoursTotal: EMPLOYEES.reduce((s, e) => s + e.weeklyHours, 0), - }; - } - - return NextResponse.json({ - generatedAt: new Date().toISOString(), - company: "MEGA INFRASTRUCTURE", - ...payload, - }); -} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts deleted file mode 100644 index a82c54f..0000000 --- a/src/app/api/upload/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { NextResponse } from "next/server"; -import type { UploadKind } from "@/lib/types"; -import { UPLOAD_ACCEPTANCE } from "@/lib/uploads"; - -const VALID_KINDS: UploadKind[] = [ - "orders", - "delivery-notes", - "fuel-logs", - "time-sheets", -]; - -function isUploadKind(value: string): value is UploadKind { - return (VALID_KINDS as string[]).includes(value); -} - -/** - * File upload endpoint. Accepts multipart/form-data with: - * - `type` — one of `orders`, `delivery-notes`, `fuel-logs`, `time-sheets` - * - `file` — one or more files matching the type's allowed MIME list - * - * Returns a JSON descriptor with the accepted and rejected files. Each - * type has its own MIME allowlist and max size (see `lib/uploads.ts`). - * - * NOTE: this is a stub — when PocketBase is wired the body will be - * forwarded to the matching collection. - */ -export async function POST(request: Request) { - const form = await request.formData(); - const typeRaw = (form.get("type") as string | null) ?? ""; - if (!isUploadKind(typeRaw)) { - return NextResponse.json( - { - error: `Missing or invalid 'type' field. Expected one of: ${VALID_KINDS.join(", ")}.`, - }, - { status: 400 } - ); - } - - const rules = UPLOAD_ACCEPTANCE[typeRaw]; - const files = form.getAll("file"); - if (files.length === 0) { - return NextResponse.json({ error: "No file provided" }, { status: 400 }); - } - - const accepted: Array<{ name: string; size: number; mime: string }> = []; - const rejected: Array<{ name: string; reason: string }> = []; - - for (const entry of files) { - if (!(entry instanceof File)) { - rejected.push({ name: "?", reason: "Not a file" }); - continue; - } - if (!rules.mimes.includes(entry.type)) { - rejected.push({ - name: entry.name, - reason: `Unsupported type ${entry.type || "unknown"} for ${typeRaw}. Expected ${rules.extensions.join(", ")}.`, - }); - continue; - } - if (entry.size > rules.maxBytes) { - const maxMb = (rules.maxBytes / 1024 / 1024).toFixed(0); - rejected.push({ - name: entry.name, - reason: `Exceeds ${maxMb}MB limit`, - }); - continue; - } - accepted.push({ name: entry.name, size: entry.size, mime: entry.type }); - } - - return NextResponse.json({ - ok: true, - type: typeRaw, - accepted, - rejected, - uploadedAt: new Date().toISOString(), - }); -} - -export async function GET() { - return NextResponse.json({ - endpoint: "/api/upload", - method: "POST", - body: "multipart/form-data with fields 'type' (one of: orders, delivery-notes, fuel-logs, time-sheets) and 'file'.", - acceptedTypes: VALID_KINDS.reduce>( - (acc, k) => { - acc[k] = { - extensions: UPLOAD_ACCEPTANCE[k].extensions, - maxBytes: UPLOAD_ACCEPTANCE[k].maxBytes, - }; - return acc; - }, - {} - ), - }); -} diff --git a/src/app/dashboard/employees/page.tsx b/src/app/dashboard/employees/page.tsx deleted file mode 100644 index 8fa2a0f..0000000 --- a/src/app/dashboard/employees/page.tsx +++ /dev/null @@ -1,378 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, - type ColumnDef, - type SortingState, -} from "@tanstack/react-table"; -import { - AlertTriangle, - Bed, - ChevronDown, - Clock, - Search, - TrendingUp, - UserCheck, - X, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Progress } from "@/components/ui/progress"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { KpiCard } from "@/components/kpi-card"; -import { WorkforceTrendBars } from "@/components/charts/workforce-trend-bars"; -import { Pagination } from "@/components/ui/pagination"; -import { EMPLOYEES, SITES } from "@/lib/mock-data"; -import type { Employee, EmployeeRole } from "@/lib/types"; -import { cn, formatCurrency } from "@/lib/utils"; - -type Flag = "ok" | "overworked" | "underworked" | "absent" | "leave"; - -const flagFor = (e: Employee): Flag => { - if (e.status === "On Leave") return "leave"; - const weekHours = e.dailyHours.slice(1).reduce((s, d) => s + d.hours, 0); - if (weekHours === 0) return "absent"; - if (e.weeklyOvertimeHours > 6) return "overworked"; - if (weekHours < 20) return "underworked"; - return "ok"; -}; - -const flagMeta: Record = { - ok: { label: "On track", variant: "success", icon: }, - overworked: { label: "Overworked", variant: "warning", icon: }, - underworked: { label: "Underworked", variant: "info", icon: }, - absent: { label: "No hours", variant: "danger", icon: }, - leave: { label: "On leave", variant: "secondary", icon: }, -}; - -const ROLES: EmployeeRole[] = [ - "Site Foreman", "Operator", "Driver", "Mechanic", "Electrician", "Welder", "Labourer", "Surveyor", "Safety Officer", -]; - -export default function EmployeesPage() { - const [search, setSearch] = React.useState(""); - const [siteFilter, setSiteFilter] = React.useState("all"); - const [roleFilter, setRoleFilter] = React.useState("all"); - const [flagFilter, setFlagFilter] = React.useState("all"); - const [sorting, setSorting] = React.useState([{ id: "weeklyHours", desc: true }]); - const [pageSize, setPageSize] = React.useState(8); - - const filtered = React.useMemo( - () => - EMPLOYEES.filter((e) => { - if (siteFilter !== "all" && e.site !== siteFilter) return false; - if (roleFilter !== "all" && e.role !== roleFilter) return false; - if (flagFilter !== "all" && flagFor(e) !== flagFilter) return false; - if (search && !`${e.fullName} ${e.role}`.toLowerCase().includes(search.toLowerCase())) - return false; - return true; - }), - [search, siteFilter, roleFilter, flagFilter] - ); - - const columns = React.useMemo[]>( - () => [ - { - accessorKey: "fullName", - header: "Employee", - cell: ({ row }) => ( -
-

{row.original.fullName}

-

{row.original.phone}

-
- ), - }, - { accessorKey: "role", header: "Role" }, - { accessorKey: "site", header: "Site" }, - { - id: "status", - header: "Status", - cell: ({ row }) => { - const f = flagFor(row.original); - return ( - - {flagMeta[f].icon} - {flagMeta[f].label} - - ); - }, - }, - { - accessorKey: "weeklyHours", - header: "This week", - cell: ({ row }) => { - const e = row.original; - const pct = Math.min(100, (e.weeklyHours / e.weeklyTargetHours) * 100); - const tone = e.weeklyOvertimeHours > 6 ? "warning" : e.weeklyHours === 0 ? "danger" : "default"; - return ( -
-
- {e.weeklyHours.toFixed(1)}h - / {e.weeklyTargetHours}h -
- -
- ); - }, - }, - { - accessorKey: "weeklyOvertimeHours", - header: "Overtime", - cell: ({ row }) => ( - 6 ? "font-semibold text-amber-600" : "text-muted-foreground" - )} - > - {row.original.weeklyOvertimeHours.toFixed(1)}h - - ), - }, - { - id: "cost", - header: "Cost", - cell: ({ row }) => ( - - {formatCurrency(row.original.weeklyHours * row.original.hourlyRate)} - - ), - }, - { - id: "trend", - header: "Trend", - cell: ({ row }) => ( - - ), - }, - ], - [] - ); - - const table = useReactTable({ - data: filtered, - columns, - state: { sorting }, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - initialState: { pagination: { pageSize } }, - }); - - React.useEffect(() => { - table.setPageSize(pageSize); - }, [pageSize, table]); - - const totalHours = EMPLOYEES.reduce((s, e) => s + e.weeklyHours, 0); - const overworked = EMPLOYEES.filter((e) => flagFor(e) === "overworked").length; - const underworked = EMPLOYEES.filter((e) => flagFor(e) === "underworked").length; - const absent = EMPLOYEES.filter((e) => flagFor(e) === "absent").length; - const onLeave = EMPLOYEES.filter((e) => flagFor(e) === "leave").length; - const totalCost = EMPLOYEES.reduce((s, e) => s + e.weeklyHours * e.hourlyRate, 0); - - return ( - <> - - Export timesheet - - } - /> - -
-
- - - - 0 ? "down" : "flat"} - hint="Includes absent + on leave" - tone={absent > 0 ? "danger" : "default"} - /> -
- - - -
- Employees - - {filtered.length} of {EMPLOYEES.length} employees · {formatCurrency(totalCost)} weekly payroll - -
-
-
- - setSearch(e.target.value)} - className="w-56 pl-8" - /> -
- - - - {(siteFilter !== "all" || roleFilter !== "all" || flagFilter !== "all" || search) && ( - - )} -
-
- -
- - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((header) => ( - - {header.isPlaceholder ? null : ( - - )} - - ))} - - ))} - - - {table.getRowModel().rows.length === 0 ? ( - - - No employees match the current filters. - - - ) : ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - )} - -
-
- -
- table.setPageIndex(p - 1)} - onPageSizeChange={(s) => setPageSize(s)} - showPageSize - pageSizeOptions={[5, 8, 12, 25]} - label="Employees" - /> -
-
-
-
- - ); -} diff --git a/src/app/dashboard/fuel/page.tsx b/src/app/dashboard/fuel/page.tsx deleted file mode 100644 index 7360c3e..0000000 --- a/src/app/dashboard/fuel/page.tsx +++ /dev/null @@ -1,388 +0,0 @@ -"use client"; - -import * as React from "react"; -import { format, parseISO } from "date-fns"; -import { - AlertTriangle, - Download, - Fuel, - ShieldAlert, - TrendingDown, - TrendingUp, - Upload, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Pagination } from "@/components/ui/pagination"; -import { KpiCard } from "@/components/kpi-card"; -import { FuelTrendChart } from "@/components/charts/fuel-trend-chart"; -import { FuelByVehicleChart } from "@/components/charts/fuel-by-vehicle-chart"; -import { EMPLOYEES, FUEL_READINGS, VEHICLES } from "@/lib/mock-data"; -import { fuelCsvTemplate, parseFuelCsv } from "@/lib/csv"; -import { - analyseFuel, - consumptionPerHour, - litresByPeriod, - severityFor, - vehiclesWithAnomalies, -} from "@/lib/fuel"; -import type { FuelAnomaly } from "@/lib/types"; -import { cn, formatCurrency, formatNumber } from "@/lib/utils"; - -type Period = "day" | "week"; - -const ANOMALY_PAGE_SIZE = 10; -const VEHICLE_PAGE_SIZE = 5; - -export default function FuelPage() { - const [period, setPeriod] = React.useState("day"); - const [vehicleFilter, setVehicleFilter] = React.useState("all"); - const [search, setSearch] = React.useState(""); - const [anomalyPage, setAnomalyPage] = React.useState(1); - const [vehiclePage, setVehiclePage] = React.useState(1); - const fileRef = React.useRef(null); - - const anomalies = React.useMemo(() => analyseFuel(FUEL_READINGS, VEHICLES), []); - const filteredReadings = React.useMemo( - () => - FUEL_READINGS.filter((r) => - vehicleFilter === "all" ? true : r.vehicleId === vehicleFilter - ), - [vehicleFilter] - ); - const trend = React.useMemo(() => litresByPeriod(filteredReadings, period), [filteredReadings, period]); - const perVehicle = React.useMemo(() => { - const map = new Map(); - for (const r of filteredReadings) { - map.set(r.vehicleId, (map.get(r.vehicleId) ?? 0) + r.litresFilled); - } - return Array.from(map.entries()).map(([id, litres]) => ({ - vehicle: VEHICLES.find((v) => v.id === id)!, - litres, - })); - }, [filteredReadings]); - - const totalLitres = filteredReadings.reduce((s, r) => s + r.litresFilled, 0); - const totalCost = filteredReadings.reduce((s, r) => s + r.litresFilled * r.unitPrice, 0); - const totalHours = filteredReadings.reduce((s, r) => s + r.hoursWorked, 0); - const flagged = anomalies.filter((a) => a.severity !== "ok"); - const perVehicleFlags = vehiclesWithAnomalies(VEHICLES, anomalies); - - const filteredAnomalies = React.useMemo( - () => - anomalies.filter((a) => { - if (vehicleFilter !== "all" && a.vehicleId !== vehicleFilter) return false; - if (search) { - const v = VEHICLES.find((x) => x.id === a.vehicleId); - if (!v) return false; - const q = search.toLowerCase(); - if (!`${v.registration} ${v.type}`.toLowerCase().includes(q)) return false; - } - return true; - }), - [anomalies, vehicleFilter, search] - ); - - // Reset pagination when filters change - React.useEffect(() => { - setAnomalyPage(1); - }, [vehicleFilter, search]); - - const anomalyPageCount = Math.max(1, Math.ceil(filteredAnomalies.length / ANOMALY_PAGE_SIZE)); - const anomalySlice = React.useMemo( - () => - filteredAnomalies.slice( - (anomalyPage - 1) * ANOMALY_PAGE_SIZE, - anomalyPage * ANOMALY_PAGE_SIZE - ), - [filteredAnomalies, anomalyPage] - ); - - const vehiclePageCount = Math.max(1, Math.ceil(perVehicleFlags.length / VEHICLE_PAGE_SIZE)); - const vehicleSlice = React.useMemo( - () => - perVehicleFlags.slice( - (vehiclePage - 1) * VEHICLE_PAGE_SIZE, - vehiclePage * VEHICLE_PAGE_SIZE - ), - [perVehicleFlags, vehiclePage] - ); - React.useEffect(() => { - if (vehiclePage > vehiclePageCount) setVehiclePage(1); - }, [vehiclePage, vehiclePageCount]); - - const handleUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - // Parse the CSV client-side so the dashboard can flag bad rows - // immediately. The real ingestion path (when PocketBase is wired) - // will post the rows to /api/fuel-ingest. - const text = await file.text(); - parseFuelCsv(text, VEHICLES); - if (fileRef.current) fileRef.current.value = ""; - }; - - const downloadTemplate = () => { - const blob = new Blob([fuelCsvTemplate()], { type: "text/csv" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "fuel-readings-template.csv"; - a.click(); - URL.revokeObjectURL(url); - }; - - return ( - <> - - - - - - } - /> - -
-
- - - - a.severity === "critical").length} critical`} - trend={flagged.length > 0 ? "down" : "flat"} - hint="Above 12% expected burn" - icon={ShieldAlert} - tone={flagged.length > 0 ? "danger" : "success"} - /> -
- - - -
- Consumption over time - Group by {period === "day" ? "day" : "week"} · {filteredReadings.length} readings -
-
- - setPeriod(v as Period)}> - - Day - Week - - -
-
- - - -
- -
- - - Litres filled per vehicle - Stack-ranked across the selected window - - - - - - - - - - - Per-vehicle anomaly count - - Critical / alert / watch - - -
    - {vehicleSlice.map(({ vehicle, critical, alert, watch }) => { - const total = critical + alert + watch; - return ( -
  • -
    -

    {vehicle.registration}

    -

    {vehicle.type} · {vehicle.assignedSite}

    -
    -
    - {critical > 0 && {critical} crit} - {alert > 0 && {alert} alert} - {watch > 0 && {watch} watch} - {total === 0 && OK} -
    -
  • - ); - })} -
- -
-
-
- - - - Anomaly log - - Expected burn = hoursWorked × expected L/h · tolerance {12}% · flagged when over - - - -
- setSearch(e.target.value)} - className="sm:max-w-xs" - /> -

{filteredAnomalies.length} entries

-
-
- - - - Date - Vehicle - Expected L - Filled L - Δ - Severity - - - - {anomalySlice.map((a) => ( - - ))} - {anomalySlice.length === 0 && ( - - - No anomalies match your filters. - - - )} - -
-
- -
-
-
- - ); -} - -function AnomalyRow({ anomaly }: { anomaly: FuelAnomaly }) { - const v = VEHICLES.find((x) => x.id === anomaly.vehicleId)!; - return ( - - {format(parseISO(anomaly.date), "d MMM")} - - {v.registration} - {v.type} - - {anomaly.expectedLitres} - {anomaly.actualLitres} - 0 ? "text-destructive" : "text-emerald-600" - )} - > - {anomaly.deltaLitres > 0 ? "+" : ""}{anomaly.deltaLitres} L - - - - {anomaly.severity} - - - - ); -} diff --git a/src/app/dashboard/orders/page.tsx b/src/app/dashboard/orders/page.tsx deleted file mode 100644 index df98fb3..0000000 --- a/src/app/dashboard/orders/page.tsx +++ /dev/null @@ -1,605 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, - type ColumnDef, - type SortingState, - type ColumnFiltersState, -} from "@tanstack/react-table"; -import { format, parseISO } from "date-fns"; -import { - ChevronDown, - FileText, - Image as ImageIcon, - Search, - X, - Upload, - CheckCircle2, - Clock, - Truck, - AlertCircle, - PackageX, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { KpiCard } from "@/components/kpi-card"; -import { OrdersStatusChart } from "@/components/charts/orders-status-chart"; -import { Pagination } from "@/components/ui/pagination"; -import { ORDERS } from "@/lib/mock-data"; -import type { Order, OrderStatus, OrderPriority } from "@/lib/types"; -import { cn, formatCurrency } from "@/lib/utils"; - -const STATUS_OPTIONS: OrderStatus[] = ["Awaiting", "Partial", "Delivered", "Discrepancy", "Cancelled"]; -const PRIORITY_OPTIONS: OrderPriority[] = ["Low", "Normal", "High", "Urgent"]; - -const statusVariant: Record = { - Awaiting: "info", - Partial: "warning", - Delivered: "success", - Discrepancy: "danger", - Cancelled: "secondary", -}; - -const statusIcon = (s: OrderStatus) => { - switch (s) { - case "Delivered": return ; - case "Awaiting": return ; - case "Partial": return ; - case "Discrepancy": return ; - case "Cancelled": return ; - } -}; - -export default function OrdersPage() { - const [search, setSearch] = React.useState(""); - const [statusFilter, setStatusFilter] = React.useState("all"); - const [priorityFilter, setPriorityFilter] = React.useState("all"); - const [siteFilter, setSiteFilter] = React.useState("all"); - const [open, setOpen] = React.useState(null); - const [sorting, setSorting] = React.useState([{ id: "placedAt", desc: true }]); - const [columnFilters, setColumnFilters] = React.useState([]); - - const sites = React.useMemo( - () => Array.from(new Set(ORDERS.map((o) => o.site))).sort(), - [] - ); - - const filtered = React.useMemo( - () => - ORDERS.filter((o) => { - if (statusFilter !== "all" && o.status !== statusFilter) return false; - if (priorityFilter !== "all" && o.priority !== priorityFilter) return false; - if (siteFilter !== "all" && o.site !== siteFilter) return false; - if (search) { - const q = search.toLowerCase(); - if ( - !`${o.reference} ${o.supplier} ${o.site}`.toLowerCase().includes(q) - ) - return false; - } - return true; - }), - [search, statusFilter, priorityFilter, siteFilter] - ); - - const columns = React.useMemo[]>( - () => [ - { - accessorKey: "reference", - header: "Reference", - cell: ({ row }) => ( - - ), - }, - { accessorKey: "supplier", header: "Supplier" }, - { accessorKey: "site", header: "Site" }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => ( - - {statusIcon(row.original.status)} - {row.original.status} - - ), - }, - { - accessorKey: "priority", - header: "Priority", - meta: { hideOnMobile: true }, - cell: ({ row }) => ( - - {row.original.priority} - - ), - }, - { - accessorKey: "placedAt", - header: "Placed", - cell: ({ row }) => format(parseISO(row.original.placedAt), "d MMM"), - }, - { - accessorKey: "expectedAt", - header: "Expected", - meta: { hideOnMobile: true }, - cell: ({ row }) => { - const overdue = - new Date(row.original.expectedAt) < new Date() && - row.original.status !== "Delivered" && - row.original.status !== "Cancelled"; - return ( - - {format(parseISO(row.original.expectedAt), "d MMM")} - - ); - }, - }, - { - id: "outstanding", - header: "Outstanding", - cell: ({ row }) => { - const outstanding = row.original.lines.filter((l) => l.quantityDelivered < l.quantityOrdered); - if (outstanding.length === 0) - return —; - return ( - - {outstanding.length} of {row.original.lines.length} - - ); - }, - }, - { - accessorKey: "totalValue", - header: () =>
Value
, - cell: ({ row }) => ( -
{formatCurrency(row.original.totalValue)}
- ), - }, - ], - [] - ); - - const table = useReactTable({ - data: filtered, - columns, - state: { sorting, columnFilters }, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - initialState: { pagination: { pageSize: 8 } }, - }); - - const stats = React.useMemo(() => { - const total = ORDERS.length; - const open = ORDERS.filter((o) => o.status !== "Delivered" && o.status !== "Cancelled").length; - const overdue = ORDERS.filter( - (o) => o.status !== "Delivered" && o.status !== "Cancelled" && new Date(o.expectedAt) < new Date() - ).length; - const totalValue = ORDERS.reduce((s, o) => s + o.totalValue, 0); - const outstandingLines = ORDERS.reduce( - (s, o) => s + o.lines.filter((l) => l.quantityDelivered < l.quantityOrdered).length, - 0 - ); - return { total, open, overdue, totalValue, outstandingLines }; - }, []); - - const statusBreakdown = React.useMemo(() => { - const palette: Record = { - Delivered: "#4f9c5b", - Partial: "#fdb13c", - Awaiting: "#006672", - Discrepancy: "#ef4444", - Cancelled: "#a3a3a3", - }; - return STATUS_OPTIONS.map((s) => ({ - name: s, - value: ORDERS.filter((o) => o.status === s).length, - color: palette[s], - })); - }, []); - - return ( - <> - - - New order - - } - /> - -
-
- - 0 ? `${stats.overdue} overdue` : undefined} - trend={stats.overdue > 0 ? "down" : "flat"} - hint="Awaiting delivery or reconciliation" - tone="info" - /> - - s.value > 0).length} states`} - hint="See donut for distribution" - tone="default" - /> -
- - - -
- Status mix - By count -
-
- -
- -
-
    - {statusBreakdown.map((s) => ( -
  • - - - {s.name} - - {s.value} -
  • - ))} -
-
-
- - - -
- Purchase orders - - {filtered.length} of {ORDERS.length} orders match the current filters - -
-
-
- - setSearch(e.target.value)} - className="w-56 pl-8" - /> -
- - - - {(statusFilter !== "all" || priorityFilter !== "all" || siteFilter !== "all" || search) && ( - - )} -
-
- -
- - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((header) => { - const hideOnMobile = (header.column.columnDef.meta as { hideOnMobile?: boolean } | undefined)?.hideOnMobile; - return ( - - {header.isPlaceholder ? null : ( - - )} - - ); - })} - - ))} - - - {table.getRowModel().rows.length === 0 ? ( - - - No orders match the current filters. - - - ) : ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const hideOnMobile = (cell.column.columnDef.meta as { hideOnMobile?: boolean } | undefined)?.hideOnMobile; - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - )) - )} - -
-
- -
- table.setPageIndex(p - 1)} - onPageSizeChange={(s) => table.setPageSize(s)} - showPageSize - pageSizeOptions={[5, 8, 12, 25]} - label="Orders" - /> -
-
-
-
- - setOpen(null)} /> - - ); -} - -function OrderDetailDialog({ order, onClose }: { order: Order | null; onClose: () => void }) { - if (!order) return null; - const outstanding = order.lines.filter((l) => l.quantityDelivered < l.quantityOrdered); - return ( - !o && onClose()}> - - -
- - {statusIcon(order.status)} - {order.status} - - - {order.priority} - -
- {order.reference} - - {order.supplier} · {order.site} - -
- -
-
-

Placed

-

{format(parseISO(order.placedAt), "d MMM yyyy, HH:mm")}

-
-
-

Expected

-

{format(parseISO(order.expectedAt), "d MMM yyyy")}

-
-
-

Value

-

{formatCurrency(order.totalValue)}

-
-
-

Notes

-

{order.notes ?? "—"}

-
-
- -
-

Line items

-
- - - - Description - Ordered - Delivered - Outstanding - - - - {order.lines.map((l) => { - const oos = l.quantityOrdered - l.quantityDelivered; - return ( - - - {l.description} - - @ {formatCurrency(l.unitPrice)}/{l.unit} - - - - {l.quantityOrdered} {l.unit} - - - {l.quantityDelivered} {l.unit} - - 0 ? "text-amber-600" : "text-emerald-600" - )} - > - {oos > 0 ? `${oos} ${l.unit}` : "—"} - - - ); - })} - -
-
-
- -
-

- Delivery notes ({order.deliveryNotes.length}) -

- {order.deliveryNotes.length === 0 ? ( -

- No delivery notes captured yet. -

- ) : ( -
    - {order.deliveryNotes.map((n) => ( -
  • -
    - - {n.mimeType.startsWith("image/") ? : } - -
    -

    {n.fileName}

    -

    - Scanned by {n.scannedBy} · {format(parseISO(n.receivedAt), "d MMM, HH:mm")} -

    -
    -
    - -
  • - ))} -
- )} -
- - {outstanding.length > 0 && ( -
- {outstanding.length} line(s) still outstanding at end of day. -
- )} - - - - - -
-
- ); -} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 6ab1f68..4ecb35c 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,468 +1,219 @@ -"use client"; - -import * as React from "react"; -import Link from "next/link"; -import { format, parseISO, subDays } from "date-fns"; import { - AlertTriangle, - ArrowRight, Banknote, - Clock, - Flame, - Fuel, - PackageCheck, - TrendingUp, + Building2, + ClipboardCheck, + Truck, Users, + Wrench, } from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { KpiCard } from "@/components/kpi-card"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { FuelTrendChart } from "@/components/charts/fuel-trend-chart"; -import { WorkforceTrendBars } from "@/components/charts/workforce-trend-bars"; -import { OrdersStatusChart } from "@/components/charts/orders-status-chart"; -import { OrderValueTrendChart } from "@/components/charts/order-value-trend-chart"; -import { EMPLOYEES, FUEL_READINGS, ORDERS, PG_EXPENSES, PLANT_HIRE_RATES, VEHICLES } from "@/lib/mock-data"; -import { - analyseFuel, - consumptionPerHour, - litresByPeriod, - vehiclesWithAnomalies, -} from "@/lib/fuel"; -import type { Order } from "@/lib/types"; +import { pbAdmin } from "@/lib/pb"; import { formatCurrency, formatNumber } from "@/lib/utils"; -import { burnBreakdown } from "@/lib/burn"; +import { Topbar } from "@/components/layout/sidebar"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import type { + EmployeeRecord, + FixedCostRecord, + PlantRecord, + SiteRecord, + VehicleRecord, +} from "@/lib/pb-types"; -export default function DashboardPage() { - const today = React.useMemo(() => new Date(), []); - const last7 = React.useMemo( - () => format(subDays(today, 6), "yyyy-MM-dd"), - [today] - ); +export const dynamic = "force-dynamic"; - const last7Readings = React.useMemo( - () => FUEL_READINGS.filter((r) => r.date >= last7), - [last7] - ); - const prior7Readings = React.useMemo( - () => FUEL_READINGS.filter((r) => r.date < last7 && r.date >= format(subDays(today, 13), "yyyy-MM-dd")), - [today] - ); +async function loadOverview() { + const pb = await pbAdmin(); - const totalLitres7 = last7Readings.reduce((s, r) => s + r.litresFilled, 0); - const priorLitres7 = prior7Readings.reduce((s, r) => s + r.litresFilled, 0); - const totalCost7 = last7Readings.reduce((s, r) => s + r.litresFilled * r.unitPrice, 0); + const [sites, employees, vehicles, plant, fixedCosts, poUnverified, dnUnverified] = + await Promise.all([ + pb.collection("sites").getList(1, 50, { sort: "name" }), + pb.collection("employees").getList(1, 1), + pb.collection("vehicles").getList(1, 1), + pb.collection("plant").getList(1, 1), + pb.collection("fixed_costs").getList(1, 500), + pb.collection("purchase_orders").getList(1, 1, { + filter: "is_verified=false", + }), + pb.collection("delivery_notes").getList(1, 1, { + filter: "is_verified=false", + }), + ]); - const anomalies = React.useMemo(() => analyseFuel(FUEL_READINGS, VEHICLES), []); - const flagged = anomalies.filter((a) => a.severity === "alert" || a.severity === "critical"); - const watch = anomalies.filter((a) => a.severity === "watch"); + const permanent = employees.totalItems; // refined below + const casuals = await pb + .collection("employees") + .getList(1, 1, { filter: "worker_type='casual'" }); + const permanents = await pb + .collection("employees") + .getList(1, 1, { filter: "worker_type='permanent'" }); - const fuelTrend = React.useMemo(() => litresByPeriod(last7Readings, "day"), [last7Readings]); + const monthlyFixedCost = fixedCosts.items + .filter((c) => c.date_logged.startsWith(formatYearMonth(new Date()))) + .reduce((sum, c) => sum + (c.cost || 0), 0); - const ordersByStatus = React.useMemo(() => { - const map = new Map(); - for (const o of ORDERS) map.set(o.status, (map.get(o.status) ?? 0) + 1); - const palette: Record = { - Delivered: "#4f9c5b", - Partial: "#fdb13c", - Awaiting: "#006672", - Discrepancy: "#ef4444", - Cancelled: "#a3a3a3", - }; - return Array.from(map.entries()).map(([name, value]) => ({ name, value, color: palette[name] ?? "#fdb13c" })); - }, []); + const costBySite = new Map(); + for (const c of fixedCosts.items) { + if (!c.date_logged.startsWith(formatYearMonth(new Date()))) continue; + costBySite.set(c.site, (costBySite.get(c.site) ?? 0) + (c.cost || 0)); + } - const overdueOrders = ORDERS.filter( - (o) => o.status !== "Delivered" && o.status !== "Cancelled" && new Date(o.expectedAt) < today - ); + return { + sites: sites.items, + employees: { + total: employees.totalItems, + permanent: permanents.totalItems, + casual: casuals.totalItems, + }, + vehicles: vehicles.totalItems, + plant: plant.totalItems, + fixedCosts: { + total: fixedCosts.totalItems, + monthly: monthlyFixedCost, + bySite: costBySite, + }, + pendingVerification: { + purchaseOrders: poUnverified.totalItems, + deliveryNotes: dnUnverified.totalItems, + }, + }; +} - // ---------- Order financial metrics ---------- - const orderFinancials = React.useMemo(() => { - const open = (o: Order) => o.status !== "Delivered" && o.status !== "Cancelled"; - const overdue = (o: Order) => - open(o) && new Date(o.expectedAt) < today; +function formatYearMonth(d: Date) { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; +} - const totalValue = ORDERS.reduce((s, o) => s + o.totalValue, 0); - const openValue = ORDERS.filter(open).reduce((s, o) => s + o.totalValue, 0); - const overdueValue = ORDERS.filter(overdue).reduce((s, o) => s + o.totalValue, 0); - const deliveredValue = ORDERS.filter((o) => o.status === "Delivered").reduce( - (s, o) => s + o.totalValue, - 0 - ); +export default async function DashboardPage() { + const data = await loadOverview(); - // Line-level delivery rate across the whole order book - const lines = ORDERS.flatMap((o) => o.lines); - const totalOrdered = lines.reduce((s, l) => s + l.quantityOrdered, 0); - const totalDelivered = lines.reduce((s, l) => s + l.quantityDelivered, 0); - const completionPct = totalOrdered === 0 ? 0 : (totalDelivered / totalOrdered) * 100; - - // Outstanding line value (only lines still missing delivery, by unit price) - const outstandingLinesValue = lines.reduce( - (s, l) => s + Math.max(0, l.quantityOrdered - l.quantityDelivered) * l.unitPrice, - 0 - ); - - return { - totalValue, - openValue, - overdueValue, - deliveredValue, - completionPct, - outstandingLinesValue, - totalOrdered, - totalDelivered, - }; - }, [today]); - - // Daily order value trend (last 14 days) split by current status - const orderValueTrend = React.useMemo(() => { - const windowStart = format(subDays(today, 13), "yyyy-MM-dd"); - const map = new Map(); - for (let i = 13; i >= 0; i--) { - const d = format(subDays(today, i), "yyyy-MM-dd"); - map.set(d, { date: d, delivered: 0, outstanding: 0, overdue: 0 }); - } - for (const o of ORDERS) { - const placed = format(parseISO(o.placedAt), "yyyy-MM-dd"); - if (placed < windowStart) continue; - const bucket = map.get(placed); - if (!bucket) continue; - if (o.status === "Delivered") bucket.delivered += o.totalValue; - else if (new Date(o.expectedAt) < today) bucket.overdue += o.totalValue; - else bucket.outstanding += o.totalValue; - } - return Array.from(map.values()); - }, [today]); - - const employeesThisWeek = EMPLOYEES; - const totalWeeklyHours = employeesThisWeek.reduce((s, e) => s + e.weeklyHours, 0); - const overworked = employeesThisWeek.filter((e) => e.weeklyOvertimeHours > 6).length; - const absent = employeesThisWeek.filter((e) => e.dailyHours.slice(1).every((d) => d.hours === 0)).length; - - const avgConsumption = consumptionPerHour(last7Readings); - const priorConsumption = consumptionPerHour(prior7Readings); - const consumptionDelta = priorConsumption === 0 ? 0 : ((avgConsumption - priorConsumption) / priorConsumption) * 100; - - // Pick the top-1 critical anomaly for the "needs attention" rail - const topFlag = flagged[0] ?? watch[0]; + const tiles = [ + { + label: "Sites", + value: formatNumber(data.sites.length), + sub: data.sites[0]?.name ?? "No sites yet", + icon: Building2, + }, + { + label: "Employees", + value: formatNumber(data.employees.total), + sub: `${data.employees.permanent} permanent · ${data.employees.casual} casual`, + icon: Users, + }, + { + label: "Vehicles", + value: formatNumber(data.vehicles), + sub: "On site", + icon: Truck, + }, + { + label: "Plant", + value: formatNumber(data.plant), + sub: "Items on site", + icon: Wrench, + }, + { + label: "Fixed costs (this month)", + value: formatCurrency(data.fixedCosts.monthly), + sub: `${data.fixedCosts.total} entries total`, + icon: Banknote, + }, + { + label: "Pending verification", + value: formatNumber( + data.pendingVerification.purchaseOrders + data.pendingVerification.deliveryNotes + ), + sub: `${data.pendingVerification.purchaseOrders} PO · ${data.pendingVerification.deliveryNotes} DN`, + icon: ClipboardCheck, + }, + ]; return ( <> - - Last CSV ingest: 2h ago - - } + title="Overview" + subtitle="Live data from the MEGA INFRASTRUCTURE PocketBase." />
-
- = priorLitres7 ? "up" : "down"} - hint={ - - - - {flagged.length + watch.length} anomalies - - · {flagged.length} critical - · R{formatNumber(totalCost7)} spent at R24.50/L - - } - icon={Fuel} - tone="warning" - /> - o.status !== "Delivered" && o.status !== "Cancelled").length}`} - delta={`${overdueOrders.length} overdue`} - trend={overdueOrders.length > 0 ? "down" : "flat"} - hint={`${ORDERS.length} total this period`} - icon={PackageCheck} - tone="info" - /> - - {ORDERS.length} purchase orders · {formatCurrency(orderFinancials.openValue)} outstanding - - } - icon={Banknote} - tone="default" - /> - +
+ {tiles.map((t) => { + const Icon = t.icon; + return ( + + + + {t.label} + + + + +
{t.value}
+

{t.sub}

+
+
+ ); + })}
-
- - -
- Order value flow — last 14 days - - Daily value of purchase orders placed, stacked by current status. - -
- - Open orders → - -
- - - -
- +
- - - - Money in flight - - Where the open spend is sitting - - -
-
- Total open - {ORDERS.filter((o) => o.status !== "Delivered" && o.status !== "Cancelled").length} -
-

{formatCurrency(orderFinancials.openValue)}

-

Value of orders not yet fully delivered

-
-
-
- Past expected - 0 ? "danger" : "success"}>{overdueOrders.length} -
-

{formatCurrency(orderFinancials.overdueValue)}

-

Already past the supplier's expected delivery date

-
-
-
- Missing units - {orderFinancials.totalOrdered - orderFinancials.totalDelivered} units -
-

{formatCurrency(orderFinancials.outstandingLinesValue)}

-

Cost of line items still outstanding

-
-
-
-
- -
- - +
- Fuel consumption — last 7 days - - Litres filled across all vehicles · Avg {avgConsumption.toFixed(1)} L/h - {priorConsumption > 0 && ( - 5 ? "ml-1 text-destructive" : "ml-1 text-emerald-600"}> - ({consumptionDelta > 0 ? "+" : ""}{consumptionDelta.toFixed(1)}% vs prior week) - - )} - -
- - Open fuel module → - -
- - - -
- - - - Orders by status - {ORDERS.length} orders in the current period - - - -
    - {ordersByStatus.map((s) => ( -
  • - - - {s.name} - - {s.value} -
  • - ))} -
-
-
-
- - - -
- - -
- Workforce this week - - Hours logged across the team · {overworked} overworked · {absent} on leave - -
- - Open workforce → - -
- -
- {employeesThisWeek.slice(0, 3).map((e) => ( -
-
-
-

{e.fullName}

-

{e.role}

-
- 6 - ? "warning" - : e.dailyHours.slice(1).every((d) => d.hours === 0) - ? "danger" - : "success" - } - > - {e.weeklyHours.toFixed(0)}h - -
-
- -
-
- ))} -
-
-
- - - - Needs attention - Live risk signals from across the dashboard - - - {flagged.slice(0, 3).map((a) => { - const v = VEHICLES.find((x) => x.id === a.vehicleId)!; - return ( -
- - - -
-

{v.registration} · {v.type}

-

- {format(parseISO(a.date), "d MMM")} · {a.actualLitres}L filled vs {a.expectedLitres}L expected -

-

- +{a.expectedDeltaPct}% over tolerance — possible missing fuel -

-
-
- ); - })} - {overdueOrders.slice(0, 2).map((o) => ( -
- - - -
-

{o.reference} · {o.supplier}

-

Expected {format(parseISO(o.expectedAt), "d MMM")} · {o.site}

-
-
- ))} - {topFlag && ( - - Investigate fuel anomalies - - )} - {!topFlag && overdueOrders.length === 0 && ( -

- All clear — no critical signals right now. + Sites +

+ Every site in the system. Click a row to drill in once + detail pages are built.

- )} -
-
-
- -
- - - Top suppliers (this period) - By value of orders placed +
+ {data.sites.length} -
    - {Array.from( - ORDERS.reduce((m, o) => { - const cur = m.get(o.supplier) ?? { count: 0, value: 0 }; - cur.count++; - cur.value += o.totalValue; - m.set(o.supplier, cur); - return m; - }, new Map()) - ) - .sort((a, b) => b[1].value - a[1].value) - .slice(0, 5) - .map(([supplier, agg]) => ( -
  • -
    -

    {supplier}

    -

    {agg.count} orders

    + {data.sites.length === 0 ? ( +

    + No sites yet. +

    + ) : ( +
      + {data.sites.map((s) => ( +
    • +
      +

      {s.name}

      +

      + {s.project_code} · {s.duration || "—"} ·{" "} + {s.hours_per_week + ? `${s.hours_per_week} hrs/week` + : "hours not set"} +

      +
      +
      +

      + {s.contract_value + ? formatCurrency(s.contract_value) + : "—"} +

      +

      + Contract value +

      +
      +
      +

      + {formatCurrency( + data.fixedCosts.bySite.get(s.id) ?? 0 + )} +

      +

      + Fixed costs / mo +

      -

      {formatCurrency(agg.value)}

    • ))} -
    - - - - - - - - Quick wins - - Actions to clear the dashboard - - - - Review {flagged.length} fuel anomalies - - - - Reconcile {overdueOrders.length} overdue orders - - - - Check {absent + overworked} attendance flags - - +
+ )}
@@ -470,90 +221,3 @@ export default function DashboardPage() { ); } - -function BurnPanel() { - const burn = React.useMemo( - () => burnBreakdown(PLANT_HIRE_RATES, VEHICLES, PG_EXPENSES), - [] - ); - const contractValue = ORDERS.reduce((s, o) => s + o.totalValue, 0); // proxy for project size - return ( -
- - - - - Burn rate — weekly vs monthly - - - Plant hire at 45h/week per unit + P&G monthly overheads, applied to every active unit on the project. - - - -
-
-
- This week - {formatNumber((burn.weekly / Math.max(1, contractValue)) * 100)}% of PO value -
-

{formatCurrency(burn.weekly)}

-
    -
  • - Plant hire - {formatCurrency(burn.weeklyHire)} -
  • -
  • - P&G (weekly share) - {formatCurrency(burn.weeklyPg)} -
  • -
-
-
-
- This month - {formatNumber((burn.monthly / Math.max(1, contractValue)) * 100)}% of PO value -
-

{formatCurrency(burn.monthly)}

-
    -
  • - Plant hire - {formatCurrency(burn.monthlyHire)} -
  • -
  • - P&G (monthly) - {formatCurrency(burn.monthlyPg)} -
  • -
-
-
-
-
- - - Cost breakdown - Where the burn comes from - - -
    -
  • - Plant hire (weekly × {VEHICLES.filter((v) => PLANT_HIRE_RATES.some((r) => r.vehicleId === v.id)).length} units) - {formatCurrency(burn.weeklyHire)} -
  • -
  • - P&G (monthly) - {formatCurrency(burn.monthlyPg)} -
  • -
  • - Total / month - {formatCurrency(burn.monthly)} -
  • -
  • - Fuel (per L) - R25.65 -
  • -
-
-
-
- ); -} diff --git a/src/app/dashboard/sites/[id]/page.tsx b/src/app/dashboard/sites/[id]/page.tsx deleted file mode 100644 index 40708dc..0000000 --- a/src/app/dashboard/sites/[id]/page.tsx +++ /dev/null @@ -1,440 +0,0 @@ -"use client"; - -import * as React from "react"; -import Link from "next/link"; -import { notFound, useRouter } from "next/navigation"; -import { format, parseISO } from "date-fns"; -import { - ArrowLeft, - Banknote, - Briefcase, - Building2, - Calendar, - Clock, - Flame, - Fuel, - Hammer, - Hourglass, - MapPin, - Package, - Truck, - TrendingUp, - Users, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Progress } from "@/components/ui/progress"; -import { KpiCard } from "@/components/kpi-card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { FuelByVehicleChart } from "@/components/charts/fuel-by-vehicle-chart"; -import { WorkforceTrendBars } from "@/components/charts/workforce-trend-bars"; -import { - EMPLOYEES, - FUEL_READINGS, - ORDERS, - PG_EXPENSES, - PLANT_HIRE_RATES, - SITES_DATA, - VEHICLES, -} from "@/lib/mock-data"; -import { summarizeSite } from "@/lib/sites"; -import type { OrderStatus, OrderPriority } from "@/lib/types"; -import { cn, formatCurrency, formatNumber } from "@/lib/utils"; - -const STATUS_BADGE: Record = { - Awaiting: "info", - Partial: "warning", - Delivered: "success", - Discrepancy: "danger", - Cancelled: "secondary", -}; - -const PRIORITY_BADGE: Record = { - Urgent: "danger", - High: "warning", - Normal: "info", - Low: "secondary", -}; - -interface PageProps { - params: Promise<{ id: string }>; -} - -export default function SiteDetailPage({ params }: PageProps) { - const router = useRouter(); - const { id } = React.use(params); - const site = SITES_DATA.find((s) => s.id === id); - if (!site) notFound(); - - const summary = React.useMemo( - () => summarizeSite(site, ORDERS, EMPLOYEES, VEHICLES, FUEL_READINGS, PLANT_HIRE_RATES, PG_EXPENSES), - [site] - ); - - const siteOrders = React.useMemo( - () => ORDERS.filter((o) => o.site === site.name), - [site.name] - ); - const siteEmployees = React.useMemo( - () => EMPLOYEES.filter((e) => e.site === site.name), - [site.name] - ); - const siteVehicles = React.useMemo( - () => VEHICLES.filter((v) => v.assignedSite === site.name), - [site.name] - ); - const siteFuelData = React.useMemo(() => { - const map = new Map(); - for (const v of siteVehicles) map.set(v.id, 0); - for (const r of FUEL_READINGS) { - if (map.has(r.vehicleId)) { - map.set(r.vehicleId, (map.get(r.vehicleId) ?? 0) + r.litresFilled); - } - } - return siteVehicles.map((v) => ({ vehicle: v, litres: map.get(v.id) ?? 0 })); - }, [siteVehicles]); - - const burnTone = - summary.burnPct >= 75 ? "danger" : summary.burnPct >= 40 ? "warning" : "default"; - const progressTone = - site.progress >= 75 ? "success" : site.progress >= 30 ? "default" : "warning"; - - return ( - <> - - - - } - /> - -
- {/* Header card */} - - -
-
- - - -
-
- {site.status} - {site.type} -
-

{site.name}

-

- - {site.address} -

-

- {site.description} -

-
-
-
-
-
Client
-
{site.client}
-
-
-
Project manager
-
{site.projectManager}
-
-
-
Start date
-
{format(parseISO(site.startDate), "d MMM yyyy")}
-
-
-
Expected end
-
{format(parseISO(site.expectedEndDate), "d MMM yyyy")}
-
-
-
Headcount target
-
{site.headcount} planned · {summary.workersActive} active
-
-
-
- -
-
-
- - Progress - - {site.progress}% -
- -

- Started {format(parseISO(site.startDate), "MMM yyyy")} · expected completion {format(parseISO(site.expectedEndDate), "MMM yyyy")} -

-
-
-
- - Contract burn - - {summary.burnPct.toFixed(1)}% -
- -

- {formatCurrency(summary.spentToDate)} of {formatCurrency(site.contractValue)} committed -

-
-
-
-
- - {/* KPIs */} -
- - = 50 ? "up" : "flat"} - hint={`${formatCurrency(summary.budgetRemaining)} budget remaining`} - icon={Hourglass} - tone={summary.burnPct >= 75 ? "danger" : "warning"} - /> - - - - -
- - {/* Active orders */} - - -
- - - Orders at this site - - - {siteOrders.length} orders · {summary.activeOrdersCount} still open - -
- - Open orders → - -
- - {siteOrders.length === 0 ? ( -

- No orders placed against this site yet. -

- ) : ( -
- - - - Reference - Supplier - Status - Priority - Expected - Value - - - - {siteOrders.slice(0, 8).map((o) => { - const overdue = - new Date(o.expectedAt) < new Date() && - o.status !== "Delivered" && - o.status !== "Cancelled"; - return ( - - - - {o.reference} - - - {o.supplier} - - {o.status} - - - {o.priority} - - - - {format(parseISO(o.expectedAt), "d MMM")} - - - - {formatCurrency(o.totalValue)} - - - ); - })} - -
- {siteOrders.length > 8 && ( -
- Showing first 8 of {siteOrders.length} orders -
- )} -
- )} -
-
- -
- {/* Fuel by vehicle */} - - - - - Fuel filled per vehicle on site - - {siteVehicles.length} vehicles assigned - - - {siteFuelData.length === 0 ? ( -

- No vehicles on site. -

- ) : ( - - )} -
-
- - {/* Workforce weekly hours */} - - - - - Workforce this week - - - {summary.workersActive} active · {formatNumber(summary.hoursThisWeek)} h logged - - - - {siteEmployees.length === 0 ? ( -

- No employees assigned to this site yet. -

- ) : ( -
    - {siteEmployees.slice(0, 6).map((e) => ( -
  • -
    -

    {e.fullName}

    -

    {e.role}

    -
    -
    - -
    -
    -

    {e.weeklyHours.toFixed(0)} h

    -

    - of {e.weeklyTargetHours}h -

    -
    -
  • - ))} -
- )} -
-
-
- - {/* Vehicles */} - - - - - Vehicles on site - - {siteVehicles.length} units - - - {siteVehicles.length === 0 ? ( -

- No vehicles on site. -

- ) : ( -
- {siteVehicles.map((v) => { - const fuel = siteFuelData.find((d) => d.vehicle.id === v.id)?.litres ?? 0; - return ( -
-
-
-

{v.registration}

-

{v.type}

-
- {v.model} -
-
-
-

Tank

-

{v.tankCapacityL} L

-
-
-

Filled

-

{formatNumber(fuel)} L

-
-
-
- ); - })} -
- )} -
-
-
- - ); -} \ No newline at end of file diff --git a/src/app/dashboard/sites/page.tsx b/src/app/dashboard/sites/page.tsx deleted file mode 100644 index afa5f65..0000000 --- a/src/app/dashboard/sites/page.tsx +++ /dev/null @@ -1,321 +0,0 @@ -"use client"; - -import * as React from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { format, parseISO } from "date-fns"; -import { - Building2, - ChevronRight, - MapPin, - Plus, - Search, - Users, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Progress } from "@/components/ui/progress"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - EMPLOYEES, - FUEL_READINGS, - ORDERS, - PG_EXPENSES, - PLANT_HIRE_RATES, - SITES_DATA, - VEHICLES, -} from "@/lib/mock-data"; -import { summarizeAllSites } from "@/lib/sites"; -import type { SiteStatus, SiteType } from "@/lib/types"; -import { cn, formatCurrency, formatNumber } from "@/lib/utils"; - -const STATUS_OPTIONS: SiteStatus[] = ["Active", "Mobilising", "On Hold", "Completed", "Warranty"]; -const TYPE_OPTIONS: SiteType[] = ["Road", "Port", "Reclamation", "Logistics", "Terminal"]; - -const statusVariant: Record = { - Active: "success", - Mobilising: "info", - "On Hold": "warning", - Completed: "secondary", - Warranty: "default", -}; - -export default function SitesPage() { - const router = useRouter(); - const [search, setSearch] = React.useState(""); - const [statusFilter, setStatusFilter] = React.useState("all"); - const [typeFilter, setTypeFilter] = React.useState("all"); - - const summaries = React.useMemo( - () => summarizeAllSites(SITES_DATA, ORDERS, EMPLOYEES, VEHICLES, FUEL_READINGS, PLANT_HIRE_RATES, PG_EXPENSES), - [] - ); - - const filtered = summaries.filter(({ site }) => { - if (statusFilter !== "all" && site.status !== statusFilter) return false; - if (typeFilter !== "all" && site.type !== typeFilter) return false; - if (search) { - const q = search.toLowerCase(); - if ( - !`${site.name} ${site.client} ${site.address}`.toLowerCase().includes(q) - ) return false; - } - return true; - }); - - const totals = React.useMemo(() => { - return summaries.reduce( - (acc, s) => ({ - contract: acc.contract + s.contractValue, - spent: acc.spent + s.spentToDate, - activeValue: acc.activeValue + s.activeOrdersValue, - hours: acc.hours + s.hoursThisWeek, - workers: acc.workers + s.workersActive, - }), - { contract: 0, spent: 0, activeValue: 0, hours: 0, workers: 0 } - ); - }, [summaries]); - - return ( - <> - - - New site - - } - /> - -
-
- - -

Contract value

-

{formatCurrency(totals.contract)}

-

Total committed across {SITES_DATA.length} sites

-
-
- - -

Spent to date

-

{formatCurrency(totals.spent)}

- -

- {totals.contract === 0 ? "0%" : `${((totals.spent / totals.contract) * 100).toFixed(1)}%`} of total contract value -

-
-
- - -

Active orders

-

{formatCurrency(totals.activeValue)}

-

Value of open POs across all sites

-
-
- - -

Workforce this week

-

{formatNumber(totals.hours)} h

-

{formatNumber(totals.workers)} active workers

-
-
-
- - - -
- All sites - - {filtered.length} of {SITES_DATA.length} sites match the current filters - -
-
-
- - setSearch(e.target.value)} - className="w-56 pl-8" - /> -
- - -
-
- -
- - - - Site - Status - Type - Progress - Contract - Spent - Active orders - Workers - - - - - {filtered.length === 0 ? ( - - - No sites match the current filters. - - - ) : ( - filtered.map(({ site, activeOrdersCount, activeOrdersValue, workersActive, spentToDate, contractValue }) => { - const burnPct = contractValue === 0 ? 0 : (spentToDate / contractValue) * 100; - return ( - router.push(`/dashboard/sites/${site.id}`)} - > - -
- - - -
- e.stopPropagation()} - > - {site.name} - -

- - {site.address} -

-
-
-
- - {site.status} - - - {site.type} - - -
- = 75 ? "success" : site.progress >= 30 ? "default" : "warning"} - className="w-24" - /> - {site.progress}% -
-
- - {formatCurrency(contractValue)} - - - = 50 && "text-destructive" - )}> - {formatCurrency(spentToDate)} - -

- {burnPct.toFixed(0)}% burned -

-
- -
{activeOrdersCount}
-

- {formatCurrency(activeOrdersValue)} -

-
- -
- - {workersActive} -
-
- - - -
- ); - }) - )} -
-
-
-
-
- - - - Site type breakdown - Total contract value by project type - - -
    - {TYPE_OPTIONS.map((t) => { - const list = SITES_DATA.filter((s) => s.type === t); - const total = list.reduce((s, x) => s + x.contractValue, 0); - return ( -
  • -

    {t}

    -

    {formatCurrency(total)}

    -

    {list.length} project{list.length === 1 ? "" : "s"}

    -
  • - ); - })} -
-
-
-
- - ); -} \ No newline at end of file diff --git a/src/app/dashboard/uploads/page.tsx b/src/app/dashboard/uploads/page.tsx deleted file mode 100644 index 5d0f87d..0000000 --- a/src/app/dashboard/uploads/page.tsx +++ /dev/null @@ -1,369 +0,0 @@ -"use client"; - -import * as React from "react"; -import { format, parseISO } from "date-fns"; -import { - AlertCircle, - CheckCircle2, - Clock, - CloudUpload, - FileSpreadsheet, - FileText, - Fuel, - Image as ImageIcon, - Truck, - Upload, - X, - type LucideIcon, -} from "lucide-react"; -import { Topbar } from "@/components/layout/sidebar"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { UPLOAD_ACCEPTANCE, UPLOAD_HISTORY } from "@/lib/uploads"; -import type { UploadEntry, UploadKind, UploadStatus } from "@/lib/types"; - -interface BlockConfig { - kind: UploadKind; - label: string; - description: string; - longDescription: string; - icon: LucideIcon; - /** Tint colour — used for the icon background, accent stripe, and badge. */ - hex: string; - /** Tailwind classes for the icon disc, kept in sync with the hex. */ - iconBg: string; - stripe: string; - badge: string; - badgeText: string; - /** Pill background for the accepted-format chips. */ - chip: string; -} - -const BLOCKS: BlockConfig[] = [ - { - kind: "orders", - label: "Orders", - description: "Bulk purchase order lines & supplier invoices", - longDescription: "Import PO lines, supplier invoices, or stock-sheet exports. CSV with vehicle registration, date, hours worked, odometer, litres, and unit price is the fastest format.", - icon: Truck, - hex: "#fdb13c", - iconBg: "bg-[#fdb13c]/15 text-[#9c6f1e] dark:text-[#fdb13c]", - stripe: "before:bg-[#fdb13c]", - badge: "bg-[#fdb13c]/15", - badgeText: "text-[#9c6f1e] dark:text-[#fdb13c]", - chip: "bg-[#fdb13c]/10 text-[#9c6f1e] dark:text-[#fdb13c]", - }, - { - kind: "delivery-notes", - label: "Delivery Notes", - description: "Scanned GRNs, supplier dockets & photos", - longDescription: "Capture scanned delivery notes, supplier GRNs, or photos of the docket. PDFs and high-resolution JPEGs are matched to the order automatically.", - icon: FileText, - hex: "#008372", - iconBg: "bg-[#008372]/15 text-[#008372] dark:text-[#3fb8a8]", - stripe: "before:bg-[#008372]", - badge: "bg-[#008372]/15", - badgeText: "text-[#008372] dark:text-[#3fb8a8]", - chip: "bg-[#008372]/10 text-[#008372] dark:text-[#3fb8a8]", - }, - { - kind: "fuel-logs", - label: "Fuel Logs", - description: "Per-vehicle daily fuel-issue exports", - longDescription: "Drop per-vehicle daily fuel-issue exports from the depot system. The CSV needs vehicle registration, date, hours worked, odometer, litres, unit price, and expected consumption.", - icon: Fuel, - hex: "#a1ae3b", - iconBg: "bg-[#a1ae3b]/15 text-[#5e6920] dark:text-[#c0cd5a]", - stripe: "before:bg-[#a1ae3b]", - badge: "bg-[#a1ae3b]/15", - badgeText: "text-[#5e6920] dark:text-[#c0cd5a]", - chip: "bg-[#a1ae3b]/10 text-[#5e6920] dark:text-[#c0cd5a]", - }, - { - kind: "time-sheets", - label: "Time Sheets", - description: "Weekly or daily payroll timesheet exports", - longDescription: "Weekly or daily timesheet exports from the payroll system. CSV/XLSX with employee id, date, and hours worked will update the workforce dashboard automatically.", - icon: FileSpreadsheet, - hex: "#4f9c5b", - iconBg: "bg-[#4f9c5b]/15 text-[#2f6940] dark:text-[#6ec082]", - stripe: "before:bg-[#4f9c5b]", - badge: "bg-[#4f9c5b]/15", - badgeText: "text-[#2f6940] dark:text-[#6ec082]", - chip: "bg-[#4f9c5b]/10 text-[#2f6940] dark:text-[#6ec082]", - }, -]; - -const STATUS_META: Record = { - Processed: { label: "Processed", variant: "success", icon: CheckCircle2 }, - Pending: { label: "Pending", variant: "warning", icon: Clock }, - Failed: { label: "Failed", variant: "danger", icon: AlertCircle }, -}; - -function formatBytes(n: number) { - if (n < 1024) return `${n} B`; - if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; - return `${(n / 1024 / 1024).toFixed(2)} MB`; -} - -export default function UploadsPage() { - const [recent, setRecent] = React.useState(UPLOAD_HISTORY); - const [busy, setBusy] = React.useState(null); - const [toast, setToast] = React.useState<{ kind: UploadKind; ok: number; fail: number } | null>(null); - const fileInputRefs = React.useRef>({ - orders: null, - "delivery-notes": null, - "fuel-logs": null, - "time-sheets": null, - }); - - const handlePick = (kind: UploadKind) => { - fileInputRefs.current[kind]?.click(); - }; - - const handleUpload = async (kind: UploadKind, fileList: FileList | null) => { - if (!fileList || fileList.length === 0) return; - setBusy(kind); - try { - const form = new FormData(); - form.set("type", kind); - for (const f of Array.from(fileList)) form.append("file", f); - const res = await fetch("/api/upload", { method: "POST", body: form }); - const json = await res.json(); - const ok = (json.accepted as Array).length; - const fail = (json.rejected as Array).length; - setToast({ kind, ok, fail }); - - // Append a synthetic entry to the local history so the UI updates immediately. - const newEntries: UploadEntry[] = Array.from(fileList).map((f, i) => ({ - id: `up-new-${Date.now()}-${i}`, - kind, - fileName: f.name, - size: f.size, - uploadedAt: new Date().toISOString(), - uploadedBy: "You", - status: i < ok ? "Processed" : "Failed", - note: i < ok ? "Just uploaded" : (json.rejected?.[0]?.reason ?? "Rejected"), - })); - setRecent((prev) => [...newEntries, ...prev]); - } catch (err) { - setToast({ kind, ok: 0, fail: fileList.length }); - } finally { - setBusy(null); - if (fileInputRefs.current[kind]) fileInputRefs.current[kind]!.value = ""; - setTimeout(() => setToast(null), 4000); - } - }; - - const handleDrop = (kind: UploadKind) => (e: React.DragEvent) => { - e.preventDefault(); - handleUpload(kind, e.dataTransfer.files); - }; - - return ( - <> - - -
-

- Each card is wired to a different upload endpoint. Files are validated - against the format and size limits shown, then forwarded to the right - place in the dashboard. -

- -
- {BLOCKS.map((block) => { - const acc = UPLOAD_ACCEPTANCE[block.kind]; - const history = recent.filter((u) => u.kind === block.kind).slice(0, 4); - return ( - - -
- - - -
-
- {block.label} - - {acc.extensions.length} formats - -
- {block.description} -
-
-
- - -

{block.longDescription}

- -
- {acc.extensions.map((ext) => ( - - {ext} - - ))} - - Max {Math.round(acc.maxBytes / 1024 / 1024)} MB - -
- - {/* Drop zone */} - - { - fileInputRefs.current[block.kind] = el; - }} - id={`upload-${block.kind}`} - type="file" - className="sr-only" - multiple - accept={acc.extensions.join(",")} - onChange={(e) => handleUpload(block.kind, e.target.files)} - /> - - - {/* Recent uploads for this kind */} -
-
-

- Recent -

- - {history.length} of {recent.filter((u) => u.kind === block.kind).length} - -
- {history.length === 0 ? ( -

- No uploads yet. -

- ) : ( -
    - {history.map((u) => { - const meta = STATUS_META[u.status]; - const Icon = meta.icon; - return ( -
  • - {u.kind === "delivery-notes" ? ( - - ) : ( - - )} -
    -

    {u.fileName}

    -

    - {formatBytes(u.size)} · {u.uploadedBy} ·{" "} - {format(parseISO(u.uploadedAt), "d MMM, HH:mm")} -

    -
    - - - {meta.label} - -
  • - ); - })} -
- )} -
-
-
- ); - })} -
- - {toast && ( -
0 ? "border-destructive" : "border-emerald-500" - )} - role="status" - aria-live="polite" - > - {toast.fail > 0 ? ( - - ) : ( - - )} -

- {toast.ok} accepted · {toast.fail} rejected -

- -
- )} -
- - ); -} diff --git a/src/components/charts/employee-hours-chart.tsx b/src/components/charts/employee-hours-chart.tsx deleted file mode 100644 index eb3664d..0000000 --- a/src/components/charts/employee-hours-chart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - Bar, - BarChart, - CartesianGrid, - Cell, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; - -interface DayHours { - date: string; - hours: number; -} - -interface Props { - data: DayHours[]; - /** Chart height in pixels. Defaults to 200. */ - height?: number; - /** When `true` (default) renders grid, axes and tooltip. Set `false` - * for a sparkline-style inline chart where the container's height - * is too small to fit axis labels. */ - showAxis?: boolean; - /** When provided, renders with a fixed pixel width instead of filling - * the parent container. Useful for compact inline sparklines. */ - width?: number; -} - -export function EmployeeHoursChart({ data, height = 200, showAxis = true, width }: Props) { - const enriched = data.map((d) => { - const tone = - d.hours === 0 ? "danger" : d.hours > 10 ? "warning" : d.hours < 6 ? "info" : "success"; - return { ...d, tone }; - }); - const margin = showAxis - ? { top: 10, right: 8, left: -16, bottom: 0 } - : { top: 4, right: 2, left: 2, bottom: 4 }; - const radius: [number, number, number, number] = showAxis ? [6, 6, 0, 0] : [3, 3, 0, 0]; - const barCell = (entry: { tone: string }, i: number) => { - const fill = - entry.tone === "danger" - ? "#ef4444" - : entry.tone === "warning" - ? "#fdb13c" - : entry.tone === "info" - ? "#006672" - : "#4f9c5b"; - return ; - }; - - const chartBody = ( - <> - {showAxis && } - {showAxis && ( - v.slice(5)} - /> - )} - {showAxis && ( - - )} - {showAxis && ( - [`${v.toFixed(1)} hrs`, "Worked"]} - /> - )} - - {enriched.map(barCell)} - - - ); - - if (width !== undefined) { - // Fixed-size rendering — works reliably in headless / SSR / narrow - // containers without depending on ResizeObserver. - return ( - - {chartBody} - - ); - } - - return ( - - - {chartBody} - - - ); -} diff --git a/src/components/charts/fuel-by-vehicle-chart.tsx b/src/components/charts/fuel-by-vehicle-chart.tsx deleted file mode 100644 index 7c0b28e..0000000 --- a/src/components/charts/fuel-by-vehicle-chart.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - Bar, - BarChart, - CartesianGrid, - Cell, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import type { Vehicle } from "@/lib/types"; - -interface Props { - data: Array<{ vehicle: Vehicle; litres: number }>; -} - -export function FuelByVehicleChart({ data }: Props) { - const chartData = data.map((d) => ({ name: d.vehicle.registration, litres: d.litres })); - const palette = ["#fdb13c", "#a1ae3b", "#4f9c5b", "#008372", "#006672", "#2f4858"]; - return ( - - - - - - - - {chartData.map((_, i) => ( - - ))} - - - - ); -} diff --git a/src/components/charts/fuel-trend-chart.tsx b/src/components/charts/fuel-trend-chart.tsx deleted file mode 100644 index f8cb04e..0000000 --- a/src/components/charts/fuel-trend-chart.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; - -interface Point { - bucket: string; - litres: number; - cost: number; - hours: number; -} - -interface Props { - data: Point[]; -} - -export function FuelTrendChart({ data }: Props) { - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/src/components/charts/order-value-trend-chart.tsx b/src/components/charts/order-value-trend-chart.tsx deleted file mode 100644 index b2fc110..0000000 --- a/src/components/charts/order-value-trend-chart.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - Bar, - BarChart, - CartesianGrid, - Legend, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; - -interface DayValue { - date: string; // yyyy-MM-dd - delivered: number; - outstanding: number; - overdue: number; -} - -interface Props { - data: DayValue[]; - formatValue: (n: number) => string; -} - -export function OrderValueTrendChart({ data, formatValue }: Props) { - return ( - - - - v.slice(5)} - /> - (v >= 1000 ? `${Math.round(v / 1000)}k` : `${v}`)} - /> - [formatValue(value), name]} - /> - - - - - - - ); -} \ No newline at end of file diff --git a/src/components/charts/orders-status-chart.tsx b/src/components/charts/orders-status-chart.tsx deleted file mode 100644 index 0012119..0000000 --- a/src/components/charts/orders-status-chart.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; - -interface Props { - data: Array<{ name: string; value: number; color: string }>; -} - -export function OrdersStatusChart({ data }: Props) { - return ( - - - - - {data.map((d, i) => ( - - ))} - - - - ); -} diff --git a/src/components/charts/workforce-trend-bars.tsx b/src/components/charts/workforce-trend-bars.tsx deleted file mode 100644 index ebf91d3..0000000 --- a/src/components/charts/workforce-trend-bars.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import * as React from "react"; -import { cn } from "@/lib/utils"; - -interface DayHours { - date: string; - hours: number; -} - -interface Props { - data: DayHours[]; - /** Chart height in pixels. Defaults to 56. */ - height?: number; - /** Maximum hours for the Y-axis. Bars are scaled relative to this. */ - maxHours?: number; - className?: string; -} - -/** - * A pure-CSS bar chart for inline hourly trend visualisation. Renders - * each day's hours as a coloured bar without depending on Recharts or - * ResizeObserver, so it works reliably in any environment (including - * strict headless renderers and tight inline containers). - */ -export function WorkforceTrendBars({ data, height = 56, maxHours = 14, className }: Props) { - const safeMax = Math.max(maxHours, ...data.map((d) => d.hours), 1); - return ( -
- {data.map((d) => { - const h = Math.max(2, (d.hours / safeMax) * height); - const tone = - d.hours === 0 - ? "bg-destructive" - : d.hours > 10 - ? "bg-amber-500" - : d.hours < 6 - ? "bg-[#006672]" - : "bg-[#4f9c5b]"; - return ( -
- ); - })} -
- ); -} \ No newline at end of file diff --git a/src/components/kpi-card.tsx b/src/components/kpi-card.tsx deleted file mode 100644 index af2c32e..0000000 --- a/src/components/kpi-card.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import * as React from "react"; -import { ArrowDownRight, ArrowUpRight, type LucideIcon } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { Card, CardContent } from "@/components/ui/card"; - -interface KpiCardProps { - label: string; - value: string; - hint?: React.ReactNode; - delta?: string; - trend?: "up" | "down" | "flat"; - icon?: LucideIcon; - tone?: "default" | "success" | "warning" | "danger" | "info"; -} - -const toneClass: Record, string> = { - default: "bg-primary/15 text-primary", - success: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400", - warning: "bg-amber-500/15 text-amber-600 dark:text-amber-400", - danger: "bg-destructive/15 text-destructive", - info: "bg-sky-500/15 text-sky-600 dark:text-sky-400", -}; - -export function KpiCard({ - label, - value, - hint, - delta, - trend, - icon: Icon, - tone = "default", -}: KpiCardProps) { - return ( - - -
-

{label}

- {Icon && ( - - - - )} -
-
-

{value}

- {delta && ( - - {trend === "up" && } - {trend === "down" && } - {delta} - - )} -
- {hint &&
{hint}
} -
-
- ); -} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 2753c5c..257da8f 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -4,15 +4,10 @@ import * as React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { - Building2, - Fuel, LayoutDashboard, - Package, LogOut, Menu, - Upload, X, - Users, } from "lucide-react"; import { Logo } from "@/components/logo"; import { ThemeToggle } from "@/components/theme-toggle"; @@ -28,13 +23,10 @@ interface NavItem { description: string; } +// Navigation entries are added page-by-page as each PocketBase-backed +// route is built. The Overview link below is the only one wired today. const NAV: NavItem[] = [ { href: "/dashboard", label: "Overview", icon: LayoutDashboard, description: "Site-at-a-glance KPIs" }, - { href: "/dashboard/fuel", label: "Fuel", icon: Fuel, description: "Consumption, theft detection" }, - { href: "/dashboard/orders", label: "Orders", icon: Package, description: "Purchase orders & delivery notes" }, - { href: "/dashboard/employees", label: "Workforce", icon: Users, description: "Weekly hours & attendance" }, - { href: "/dashboard/sites", label: "Sites", icon: Building2, description: "Per-site financials & progress" }, - { href: "/dashboard/uploads", label: "Uploads", icon: Upload, description: "Drop files into the right module" }, ]; export function Sidebar() { @@ -101,12 +93,6 @@ export function Sidebar() {
{item.label} - {item.label === "Fuel" && ( - 3 alerts - )} - {item.label === "Orders" && ( - 4 due - )}

{item.description}

diff --git a/src/lib/burn.ts b/src/lib/burn.ts deleted file mode 100644 index 27fb741..0000000 --- a/src/lib/burn.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { - PgExpense, - PlantHireRate, - Vehicle, -} from "./types"; - -/** - * Constants used by the burn-rate computations on the dashboard. - * Tweak these if the project shifts from a 45-hour work week. - */ -export const STANDARD_WEEKLY_HOURS = 45; - -/** - * Given a list of plant hire rates and the vehicles on this site, - * compute the weekly hire cost. Vehicles without a hire rate - * contribute zero — they're typically owned or supplied by the - * client and only their fuel is tracked. - */ -export function weeklyPlantHireCost( - hireRates: PlantHireRate[], - siteVehicles: Vehicle[] -): number { - const rateByVehicle = new Map(hireRates.map((r) => [r.vehicleId, r.ratePerHour])); - let total = 0; - for (const v of siteVehicles) { - const rate = rateByVehicle.get(v.id); - if (typeof rate === "number") total += rate * STANDARD_WEEKLY_HOURS; - } - return total; -} - -/** - * Sum of every P&G line on this site for a calendar month. - */ -export function monthlyPgCost(expenses: PgExpense[]): number { - return expenses.reduce((s, e) => s + e.monthlyAmount, 0); -} - -/** - * Convert a monthly ZAR figure into a per-week equivalent using the - * standard 4.345 weeks-per-month convention. - */ -export function weeklyFromMonthly(monthlyZar: number): number { - return monthlyZar / 4.345; -} - -/** - * Convenience wrapper — weekly burn = weekly plant hire + weekly P&G - * share. Returns both component sums so the dashboard can display them - * separately. - */ -export function burnBreakdown( - hireRates: PlantHireRate[], - siteVehicles: Vehicle[], - pgExpenses: PgExpense[] -): { weeklyHire: number; weeklyPg: number; monthlyHire: number; monthlyPg: number; weekly: number; monthly: number } { - const weeklyHire = weeklyPlantHireCost(hireRates, siteVehicles); - const monthlyHire = (weeklyHire / 7) * 30; - const monthlyPg = monthlyPgCost(pgExpenses); - const weeklyPg = weeklyFromMonthly(monthlyPg); - return { - weeklyHire, - weeklyPg, - monthlyHire, - monthlyPg, - weekly: weeklyHire + weeklyPg, - monthly: monthlyHire + monthlyPg, - }; -} diff --git a/src/lib/csv.ts b/src/lib/csv.ts deleted file mode 100644 index f1b9325..0000000 --- a/src/lib/csv.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Papa from "papaparse"; -import type { FuelReading, Vehicle } from "./types"; - -/** - * Expected CSV columns (header row required): - * vehicleRegistration,date,hoursWorked,odometer,litresFilled,unitPrice,expectedConsumptionPerHour - * - * `date` is yyyy-mm-dd. Trailing rows with invalid data are skipped with a warning. - */ -export interface ParsedFuelRow { - ok: boolean; - reason?: string; - data?: Omit & { vehicleRegistration: string }; -} - -export function parseFuelCsv( - fileContent: string, - vehicles: Vehicle[] -): { rows: ParsedFuelRow[]; errors: string[] } { - const parsed = Papa.parse>(fileContent, { - header: true, - skipEmptyLines: true, - transformHeader: (h) => h.trim(), - }); - const errors: string[] = parsed.errors.map((e) => `row ${e.row}: ${e.message}`); - const byReg = new Map(vehicles.map((v) => [v.registration.toUpperCase(), v])); - const rows: ParsedFuelRow[] = []; - for (const raw of parsed.data) { - const reg = (raw.vehicleRegistration || "").trim().toUpperCase(); - const v = byReg.get(reg); - if (!v) { - rows.push({ ok: false, reason: `Unknown registration "${reg}"` }); - continue; - } - const date = (raw.date || "").trim(); - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - rows.push({ ok: false, reason: `Invalid date "${date}" (expected yyyy-mm-dd)` }); - continue; - } - const hoursWorked = Number(raw.hoursWorked); - const odometer = Number(raw.odometer); - const litresFilled = Number(raw.litresFilled); - const unitPrice = Number(raw.unitPrice); - const expectedConsumptionPerHour = Number(raw.expectedConsumptionPerHour || 0) || v.type === "Generator" ? 6 : 9; - if (![hoursWorked, odometer, litresFilled, unitPrice].every(Number.isFinite)) { - rows.push({ ok: false, reason: "Numeric fields are not all valid numbers" }); - continue; - } - rows.push({ - ok: true, - data: { - vehicleId: v.id, - vehicleRegistration: v.registration, - date, - hoursWorked, - odometer, - litresFilled, - unitPrice, - expectedConsumptionPerHour, - }, - }); - } - return { rows, errors }; -} - -export function fuelCsvTemplate(): string { - return [ - "vehicleRegistration,date,hoursWorked,odometer,litresFilled,unitPrice,expectedConsumptionPerHour", - "MI-EX-01,2026-08-01,9.5,12480,90,24.50,9", - "MI-TT-07,2026-08-01,7.0,88412,55,24.50,7", - "MI-LD-03,2026-08-01,8.0,32104,80,24.50,10", - ].join("\n"); -} diff --git a/src/lib/fuel.ts b/src/lib/fuel.ts deleted file mode 100644 index abea025..0000000 --- a/src/lib/fuel.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { FuelReading, FuelAnomaly, Vehicle } from "./types"; - -/** - * Compare today's reading against the previous reading on the same vehicle - * to compute how many litres *should* have been used, based on hours worked. - * - * expectedLitres = hoursWorkedToday * expectedConsumptionPerHour - * - * If actualLitresFilled > expectedLitres by more than the tolerance, we flag - * it as missing-fuel / potential theft. - * - * NOTE: This is the production formula we'll use at site — change - * EXPECTED_LITRES_TOLERANCE_PCT if we want a different threshold. - */ -export const EXPECTED_LITRES_TOLERANCE_PCT = 12; // % above expected before we alert - -export function severityFor(expected: number, actual: number): { - severity: FuelAnomaly["severity"]; - deltaLitres: number; - deltaPct: number; -} { - if (expected <= 0) { - return { severity: "ok", deltaLitres: 0, deltaPct: 0 }; - } - const deltaLitres = actual - expected; - const deltaPct = (deltaLitres / expected) * 100; - const over = Math.max(0, deltaPct); - let severity: FuelAnomaly["severity"] = "ok"; - if (over > EXPECTED_LITRES_TOLERANCE_PCT) severity = "watch"; - if (over > EXPECTED_LITRES_TOLERANCE_PCT * 2) severity = "alert"; - if (over > EXPECTED_LITRES_TOLERANCE_PCT * 4) severity = "critical"; - return { severity, deltaLitres, deltaPct }; -} - -export function analyseFuel( - readings: FuelReading[], - vehicles: Vehicle[] -): FuelAnomaly[] { - const byVehicle = new Map(); - for (const r of readings) { - const arr = byVehicle.get(r.vehicleId) ?? []; - arr.push(r); - byVehicle.set(r.vehicleId, arr); - } - const anomalies: FuelAnomaly[] = []; - for (const [, list] of byVehicle) { - const sorted = [...list].sort((a, b) => a.date.localeCompare(b.date)); - for (let i = 0; i < sorted.length; i++) { - const r = sorted[i]; - const expected = r.hoursWorked * r.expectedConsumptionPerHour; - const { severity, deltaLitres, deltaPct } = severityFor(expected, r.litresFilled); - anomalies.push({ - readingId: r.id, - vehicleId: r.vehicleId, - date: r.date, - expectedLitres: Number(expected.toFixed(2)), - actualLitres: r.litresFilled, - deltaLitres: Number(deltaLitres.toFixed(2)), - expectedDeltaPct: Number(deltaPct.toFixed(1)), - severity, - }); - } - } - // Sort by date desc, then by severity desc (critical → alert → watch → ok) - // so the most actionable entries surface first in the log. - const severityWeight: Record = { - critical: 4, - alert: 3, - watch: 2, - ok: 1, - }; - anomalies.sort((a, b) => { - const dateDiff = b.date.localeCompare(a.date); - if (dateDiff !== 0) return dateDiff; - return severityWeight[b.severity] - severityWeight[a.severity]; - }); - return anomalies; -} - -export function litresByPeriod( - readings: FuelReading[], - granularity: "hour" | "day" | "week" -): Array<{ bucket: string; litres: number; cost: number; hours: number }> { - const buckets = new Map(); - for (const r of readings) { - let key = r.date; - if (granularity === "week") { - const d = new Date(r.date); - const tmp = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); - const dayNum = tmp.getUTCDay() || 7; - tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1)); - const weekNo = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); - key = `${tmp.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`; - } - const cur = buckets.get(key) ?? { litres: 0, cost: 0, hours: 0 }; - cur.litres += r.litresFilled; - cur.cost += r.litresFilled * r.unitPrice; - cur.hours += r.hoursWorked; - buckets.set(key, cur); - } - return Array.from(buckets.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([bucket, v]) => ({ bucket, ...v })); -} - -export function consumptionPerHour(readings: FuelReading[]): number { - const totalLitres = readings.reduce((s, r) => s + r.litresFilled, 0); - const totalHours = readings.reduce((s, r) => s + r.hoursWorked, 0); - if (totalHours === 0) return 0; - return totalLitres / totalHours; -} - -export function vehiclesWithAnomalies( - vehicles: Vehicle[], - anomalies: FuelAnomaly[] -): Array<{ vehicle: Vehicle; critical: number; alert: number; watch: number }> { - const map = new Map(); - for (const v of vehicles) map.set(v.id, { critical: 0, alert: 0, watch: 0 }); - for (const a of anomalies) { - const cur = map.get(a.vehicleId); - if (!cur) continue; - if (a.severity === "critical") cur.critical++; - if (a.severity === "alert") cur.alert++; - if (a.severity === "watch") cur.watch++; - } - return vehicles.map((vehicle) => ({ - vehicle, - ...(map.get(vehicle.id) ?? { critical: 0, alert: 0, watch: 0 }), - })); -} diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts deleted file mode 100644 index 3028767..0000000 --- a/src/lib/mock-data.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - addDays, - format, - startOfDay, - startOfMonth, - startOfWeek, - subDays, -} from "date-fns"; -import type { - DeliveryNote, - Employee, - FuelReading, - Order, - OrderLine, - PgExpense, - PlantHireRate, - Site, - Vehicle, -} from "./types"; - -// --------------------------------------------------------------------------- -// PLACEHOLDER DATA LAYER -// --------------------------------------------------------------------------- -// Every array below is empty by design. The real source of truth will -// be PocketBase (https://pb.mega.infra.apphub.co.za) — see lib/pb.ts for -// the wiring that swaps these exports for live PB queries. -// -// Until PB is connected, the dashboard renders in its empty-state UI: -// • Dashboard KPIs all show 0 -// • Charts draw empty axes -// • Tables say "No X match the current filters" -// • Upload history is blank -// • The Sites list shows the empty-state card -// • The site detail route at /dashboard/sites/[id] returns a 404 -// -// The site helpers (SITES, SUPPLIERS) and the type definitions stay in -// place so that all pages continue to render — they just show zeros -// and empty lists until live data is wired in. -// --------------------------------------------------------------------------- - -export const SITES_DATA: Site[] = []; - -export const SITES: string[] = []; - -export const SUPPLIERS: string[] = []; - -export const VEHICLES: Vehicle[] = []; - -export const EMPLOYEES: Employee[] = []; - -export const ORDERS: Order[] = []; - -export const FUEL_READINGS: FuelReading[] = []; - -export const PLANT_HIRE_RATES: PlantHireRate[] = []; - -export const PG_EXPENSES: PgExpense[] = []; - -export { - addDays, - format, - startOfDay, - startOfMonth, - startOfWeek, - subDays, -}; - -export type { - DeliveryNote, - Employee, - FuelReading, - Order, - OrderLine, - PgExpense, - PlantHireRate, - Site, - Vehicle, -}; diff --git a/src/lib/pb-types.ts b/src/lib/pb-types.ts new file mode 100644 index 0000000..f3de03b --- /dev/null +++ b/src/lib/pb-types.ts @@ -0,0 +1,157 @@ +/** + * Typed shapes matching the 12 MEGA INFRASTRUCTURE collections on + * `https://pb.mega.infra.apphub.co.za`. Keep in sync with the live + * schema — re-run `scripts/pb-bootstrap-schema.py` after changes. + */ +import type { RecordModel } from "./pb"; + +export interface SiteRecord extends RecordModel { + collectionName: "sites"; + project_code: string; + name: string; + duration: string; + contract_value: number; + work_hours: number; + hours_per_week: number; + wages_hours_per_day: number; + expand?: Record; +} + +export type WorkerType = "permanent" | "casual"; + +export interface EmployeeRecord extends RecordModel { + collectionName: "employees"; + project_code: string; + site: string; // relation id + name: string; + employee_no: string; + position: string; + rate_per_hour: number; + hours_per_day_standard: number; + worker_type: WorkerType; + expand?: { site?: SiteRecord }; +} + +export interface VehicleRecord extends RecordModel { + collectionName: "vehicles"; + project_code: string; + site: string; + description: string; + standard_work_hours: string | number; + fuel_limits: string; + expand?: { site?: SiteRecord }; +} + +export interface PlantRecord extends RecordModel { + collectionName: "plant"; + project_code: string; + site: string; + description: string; + standard_work_hours: string | number; + cost_per_hour: number; + expand?: { site?: SiteRecord }; +} + +export interface FixedCostRecord extends RecordModel { + collectionName: "fixed_costs"; + project_code: string; + site: string; + date_logged: string; + description: string; + cost: number; + notes: string; + expand?: { site?: SiteRecord }; +} + +export type BatchType = "timesheets" | "fuel_logs"; + +export interface DataImportBatchRecord extends RecordModel { + collectionName: "data_import_batches"; + batch_type: BatchType; + original_file: string; + date_uploaded: string; + uploaded_by: string; +} + +export interface PurchaseOrderRecord extends RecordModel { + collectionName: "purchase_orders"; + purchase_order_number: string; + project_code: string; + site: string; + supplier: string; + required_by_date: string; + ordered_by: string; + items_count: number; + total_value: number; + purchase_order_file: string; + raw_ocr_data: unknown; + is_verified: boolean; + expand?: { site?: SiteRecord }; +} + +export interface PurchaseOrderItemRecord extends RecordModel { + collectionName: "purchase_order_items"; + purchase_order: string; + material_code: string; + cost_code: string; + description: string; + qty: number; + qty_outstanding: number; + unit: string; + rate: number; + disc_percent: number; + vat_percent: number; + amount: number; + expand?: { purchase_order?: PurchaseOrderRecord }; +} + +export interface TimesheetRecord extends RecordModel { + collectionName: "timesheets"; + employee: string; + batch_reference: string; + date_logged: string; + hours_worked: number; + expand?: { employee?: EmployeeRecord; batch_reference?: DataImportBatchRecord }; +} + +export interface FuelLogRecord extends RecordModel { + collectionName: "fuel_logs"; + project_code: string; + site: string; + vehicle: string; + batch_reference: string; + date_logged: string; + litres: number; + odo_reading: number; + expand?: { + site?: SiteRecord; + vehicle?: VehicleRecord; + batch_reference?: DataImportBatchRecord; + }; +} + +export interface DeliveryNoteRecord extends RecordModel { + collectionName: "delivery_notes"; + delivery_note_number: string; + project_code: string; + site: string; + purchase_order: string; + date_received: string; + received_by: string; + slip_image: string; + raw_ocr_data: unknown; + is_verified: boolean; + expand?: { site?: SiteRecord; purchase_order?: PurchaseOrderRecord }; +} + +export interface ReceivedItemRecord extends RecordModel { + collectionName: "received_items"; + delivery_note: string; + purchase_order_item: string; + qty_received: number; + condition_notes: string; + expand?: { + delivery_note?: DeliveryNoteRecord; + purchase_order_item?: PurchaseOrderItemRecord; + }; +} diff --git a/src/lib/pb.ts b/src/lib/pb.ts new file mode 100644 index 0000000..78778fa --- /dev/null +++ b/src/lib/pb.ts @@ -0,0 +1,92 @@ +/** + * PocketBase client for MEGA INFRASTRUCTURE. + * + * Two clients are exported: + * - `pb` — browser/client-safe client (uses NEXT_PUBLIC_POCKETBASE_URL). + * Reads its auth token from localStorage so the user + * stays signed in across reloads. + * - `pbAdmin` — server-only client authenticated as the superuser. + * Uses POCKETBASE_ADMIN_EMAIL + POCKETBASE_ADMIN_PASSWORD + * from the server environment and caches the token in + * module scope. + * + * Always import from this file. Do not call `new Pocketbase(...)` elsewhere. + */ +import PocketBase from "pocketbase"; + +export const PB_URL = + process.env.NEXT_PUBLIC_POCKETBASE_URL ?? "http://127.0.0.1:8090"; + +const AUTH_STORAGE_KEY = "mega-pb-auth"; + +/* ------------------------------------------------------------------ */ +/* Browser client */ +/* ------------------------------------------------------------------ */ + +export const pb = new PocketBase(PB_URL); + +// Restore persisted auth on first access (browser only). +if (typeof window !== "undefined") { + try { + const raw = window.localStorage.getItem(AUTH_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as { token?: string; model?: unknown }; + if (parsed.token) { + pb.authStore.save(parsed.token, parsed.model as never); + } + } + } catch { + /* localStorage may be blocked — ignore */ + } + pb.authStore.onChange(() => { + try { + if (pb.authStore.isValid) { + window.localStorage.setItem( + AUTH_STORAGE_KEY, + JSON.stringify({ token: pb.authStore.token, model: pb.authStore.model }) + ); + } else { + window.localStorage.removeItem(AUTH_STORAGE_KEY); + } + } catch { + /* ignore */ + } + }, true); +} + +/* ------------------------------------------------------------------ */ +/* Admin client (server-only) */ +/* ------------------------------------------------------------------ */ + +let _adminClient: PocketBase | null = null; +let _adminToken: string | null = null; + +export async function pbAdmin(): Promise { + if (_adminClient && _adminToken) return _adminClient; + + const email = process.env.POCKETBASE_ADMIN_EMAIL; + const password = process.env.POCKETBASE_ADMIN_PASSWORD; + const presetToken = process.env.POCKETBASE_ADMIN_TOKEN; + + if (!email || !password) { + throw new Error( + "pbAdmin(): POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD are not set in the server environment." + ); + } + + const client = new PocketBase(PB_URL); + if (presetToken) { + client.authStore.save(presetToken, null); + } else { + await client.collection("_superusers").authWithPassword(email, password); + } + _adminClient = client; + _adminToken = client.authStore.token; + return client; +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +export type { RecordModel } from "pocketbase"; diff --git a/src/lib/sites.ts b/src/lib/sites.ts deleted file mode 100644 index 437d7ba..0000000 --- a/src/lib/sites.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { - Employee, - FuelReading, - Order, - PgExpense, - PlantHireRate, - Site, - SiteFinancialSummary, - Vehicle, -} from "./types"; -import { format, startOfMonth } from "date-fns"; -import { burnBreakdown } from "./burn"; - -/** - * Compute a financial + operational rollup for a single site by joining - * the site record with the orders, employees and vehicles currently - * assigned to it. Fuel cost is summed across the current calendar month - * for vehicles assigned to this site. - * - * Returned values are best-effort estimates derived from mock data — - * swap them out with proper PocketBase queries once the backend is live. - */ -export function summarizeSite( - site: Site, - orders: Order[], - employees: Employee[], - vehicles: Vehicle[], - fuelReadings: FuelReading[], - hireRates: PlantHireRate[], - pgExpenses: PgExpense[], - now: Date = new Date() -): SiteFinancialSummary { - const monthStart = format(startOfMonth(now), "yyyy-MM-dd"); - - const siteName = site.name; - const siteOrders = orders.filter((o) => o.site === siteName); - const openOrders = siteOrders.filter( - (o) => o.status !== "Delivered" && o.status !== "Cancelled" - ); - const deliveredOrders = siteOrders.filter((o) => o.status === "Delivered"); - - const totalOrdersValue = siteOrders.reduce((s, o) => s + o.totalValue, 0); - const activeOrdersValue = openOrders.reduce((s, o) => s + o.totalValue, 0); - const deliveredValue = deliveredOrders.reduce((s, o) => s + o.totalValue, 0); - - const siteVehicles = vehicles.filter((v) => v.assignedSite === siteName); - const siteVehicleIds = new Set(siteVehicles.map((v) => v.id)); - - const monthFuel = fuelReadings.filter( - (r) => r.date >= monthStart && siteVehicleIds.has(r.vehicleId) - ); - const fuelCostThisMonth = monthFuel.reduce( - (s, r) => s + r.litresFilled * r.unitPrice, - 0 - ); - const fuelLitresThisMonth = monthFuel.reduce((s, r) => s + r.litresFilled, 0); - - const siteEmployees = employees.filter((e) => e.site === siteName); - const activeEmployees = siteEmployees.filter((e) => e.status === "Active"); - const hoursThisWeek = siteEmployees.reduce((s, e) => s + e.weeklyHours, 0); - - // Spent to date = open orders value + delivered order value + month fuel - // (fuel for previous months is rolled into the contract too). - const spentToDate = totalOrdersValue + fuelCostThisMonth; - const budgetRemaining = Math.max(0, site.contractValue - spentToDate); - const burnPct = site.contractValue === 0 - ? 0 - : (spentToDate / site.contractValue) * 100; - - const burn = burnBreakdown(hireRates, siteVehicles, pgExpenses); - - return { - site, - contractValue: site.contractValue, - spentToDate, - budgetRemaining, - burnPct, - activeOrdersCount: openOrders.length, - activeOrdersValue, - deliveredValue, - totalOrdersValue, - hoursThisWeek, - workersActive: activeEmployees.length, - vehiclesOnSite: siteVehicles.length, - fuelCostThisMonth, - fuelLitresThisMonth, - weeklyBurn: burn.weekly, - monthlyBurn: burn.monthly, - weeklyPlantHire: burn.weeklyHire, - monthlyPlantHire: burn.monthlyHire, - weeklyPg: burn.weeklyPg, - monthlyPg: burn.monthlyPg, - }; -} - -export function summarizeAllSites( - sites: Site[], - orders: Order[], - employees: Employee[], - vehicles: Vehicle[], - fuelReadings: FuelReading[], - hireRates: PlantHireRate[], - pgExpenses: PgExpense[], - now: Date = new Date() -): SiteFinancialSummary[] { - return sites.map((s) => summarizeSite(s, orders, employees, vehicles, fuelReadings, hireRates, pgExpenses, now)); -} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index 764126e..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,227 +0,0 @@ -// Shared domain types for the MEGA INFRASTRUCTURE dashboard. - -export type VehicleType = - | "Excavator" - | "Tipper Truck" - | "Loader" - | "Generator" - | "Grader" - | "Bulldozer" - | "Roller" - | "TLB" - | "Pickup" - | "Water Truck" - | "Compressor" - | "Mixer"; -export type FuelUnit = "L" | "gal"; - -export interface Vehicle { - id: string; - registration: string; - type: VehicleType; - model: string; - tankCapacityL: number; - assignedSite: string; - /** - * Optional km/L allowance for vehicles where fuel consumption is - * tracked by distance rather than hours (e.g. pickups, water trucks). - * If omitted, the fuel-anomaly check falls back to `hoursWorked * expectedLPerHour`. - */ - kmPerL?: number; - /** - * Optional fallback L/h allowance for plant where fuel is tracked - * against engine hours (excavators, rollers, TLBs, etc.). - * If omitted, vehicles without `kmPerL` are excluded from the anomaly - * detection rather than treated as zero-consumption. - */ - expectedLPerHour?: number; -} - -export interface FuelReading { - id: string; - vehicleId: string; - date: string; // ISO yyyy-mm-dd - hoursWorked: number; - odometer: number; - litresFilled: number; - unitPrice: number; - expectedConsumptionPerHour: number; // L/h - notes?: string; - attachmentUrl?: string; -} - -export interface FuelAnomaly { - readingId: string; - vehicleId: string; - date: string; - expectedLitres: number; - actualLitres: number; - deltaLitres: number; // positive = missing fuel, negative = over-recorded - expectedDeltaPct: number; - severity: "ok" | "watch" | "alert" | "critical"; -} - -export type OrderStatus = "Awaiting" | "Partial" | "Delivered" | "Discrepancy" | "Cancelled"; -export type OrderPriority = "Low" | "Normal" | "High" | "Urgent"; - -export interface OrderLine { - id: string; - description: string; - quantityOrdered: number; - quantityDelivered: number; - unit: string; - unitPrice: number; -} - -export interface DeliveryNote { - id: string; - orderId: string; - receivedAt: string; // ISO timestamp - scannedBy: string; - fileName: string; - fileUrl: string; - mimeType: string; -} - -export interface Order { - id: string; - reference: string; - supplier: string; - site: string; - status: OrderStatus; - priority: OrderPriority; - placedAt: string; // ISO timestamp - expectedAt: string; // ISO timestamp - totalValue: number; - notes?: string; - lines: OrderLine[]; - deliveryNotes: DeliveryNote[]; -} - -export type EmployeeRole = - | "Site Foreman" - | "Operator" - | "Driver" - | "Mechanic" - | "Electrician" - | "Welder" - | "Labourer" - | "Surveyor" - | "Safety Officer"; - -export type EmployeeStatus = "Active" | "On Leave" | "Suspended"; - -export interface DailyHours { - date: string; // ISO yyyy-mm-dd - hours: number; -} - -export interface Employee { - id: string; - fullName: string; - role: EmployeeRole; - site: string; - status: EmployeeStatus; - phone: string; - hourlyRate: number; - weeklyTargetHours: number; - weeklyHours: number; // current ISO week hours - weeklyOvertimeHours: number; - dailyHours: DailyHours[]; // last 7 days for chart -} - -export interface DashboardKpi { - label: string; - value: string; - delta?: string; - trend?: "up" | "down" | "flat"; - hint?: string; - iconName: string; - tone?: "default" | "success" | "warning" | "danger" | "info"; -} - -export type SiteStatus = "Active" | "Mobilising" | "On Hold" | "Completed" | "Warranty"; -export type SiteType = "Road" | "Port" | "Reclamation" | "Logistics" | "Terminal"; - -export interface Site { - id: string; - name: string; - client: string; - status: SiteStatus; - type: SiteType; - contractValue: number; - progress: number; // 0-100 - startDate: string; // ISO yyyy-mm-dd - expectedEndDate: string; // ISO yyyy-mm-dd - projectManager: string; - address: string; - headcount: number; - description: string; -} - -export interface SiteFinancialSummary { - site: Site; - contractValue: number; - spentToDate: number; - budgetRemaining: number; - burnPct: number; // spent / contract * 100 - activeOrdersCount: number; - activeOrdersValue: number; - deliveredValue: number; - totalOrdersValue: number; - hoursThisWeek: number; - workersActive: number; - vehiclesOnSite: number; - fuelCostThisMonth: number; - fuelLitresThisMonth: number; - weeklyBurn: number; - monthlyBurn: number; - weeklyPlantHire: number; - monthlyPlantHire: number; - weeklyPg: number; - monthlyPg: number; -} - -/** Plant hire rate — what it costs per operational hour, in ZAR. */ -export interface PlantHireRate { - vehicleId: string; - ratePerHour: number; -} - -export type PgCategory = - | "container-hire" - | "security" - | "salaries" - | "overheads" - | "insurance" - | "site-facilities" - | "fuel"; - -export interface PgExpense { - id: string; - category: PgCategory; - label: string; - /** ZAR per calendar month. */ - monthlyAmount: number; -} - -export type UploadKind = "orders" | "delivery-notes" | "fuel-logs" | "time-sheets"; -export type UploadStatus = "Processed" | "Pending" | "Failed"; - -export interface UploadEntry { - id: string; - kind: UploadKind; - fileName: string; - size: number; // bytes - uploadedAt: string; // ISO timestamp - uploadedBy: string; - status: UploadStatus; - note?: string; -} - -export interface UploadAcceptance { - mimes: string[]; // e.g. ["text/csv", "application/vnd.ms-excel"] - extensions: string[]; // e.g. [".csv", ".xlsx"] - maxBytes: number; - description: string; -} diff --git a/src/lib/uploads.ts b/src/lib/uploads.ts deleted file mode 100644 index 649e81f..0000000 --- a/src/lib/uploads.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { UploadEntry, UploadKind, UploadAcceptance } from "./types"; - -// --------------------------------------------------------------------------- -// Upload acceptance rules — kept populated so the UI on /dashboard/uploads -// can still render its format / size hints and the /api/upload endpoint -// can still validate incoming files. Once PocketBase is wired, the -// history below will be replaced with live queries to the appropriate -// PB collection per kind. -// --------------------------------------------------------------------------- - -export const UPLOAD_ACCEPTANCE: Record = { - orders: { - mimes: ["text/csv", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], - extensions: [".csv", ".xls", ".xlsx"], - maxBytes: 10 * 1024 * 1024, // 10 MB - description: "Bulk purchase order lines, supplier invoices, or stock-sheet exports.", - }, - "delivery-notes": { - mimes: ["application/pdf", "image/png", "image/jpeg", "image/webp"], - extensions: [".pdf", ".png", ".jpg", ".jpeg", ".webp"], - maxBytes: 20 * 1024 * 1024, // 20 MB - description: "Scanned delivery notes, supplier GRNs, or photos of the docket.", - }, - "fuel-logs": { - mimes: ["text/csv", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], - extensions: [".csv", ".xls", ".xlsx"], - maxBytes: 5 * 1024 * 1024, // 5 MB - description: "Per-vehicle daily fuel-issue exports from the depot system.", - }, - "time-sheets": { - mimes: ["text/csv", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], - extensions: [".csv", ".xls", ".xlsx"], - maxBytes: 5 * 1024 * 1024, - description: "Weekly or daily timesheet exports from the payroll system.", - }, -}; - -// --------------------------------------------------------------------------- -// Upload history — empty until PocketBase is wired. The shape of each -// entry matches `UploadEntry`; once your ingest pipeline lands in -// lib/pb.ts, append live records here and the Uploads page will render -// them (or replace this export entirely with a PB query). -// --------------------------------------------------------------------------- - -export const UPLOAD_HISTORY: UploadEntry[] = []; - -// Helper for callers that want to filter by kind. Returns an empty array -// when `UPLOAD_HISTORY` is empty. -export function uploadsByKind(kind: UploadKind): UploadEntry[] { - return UPLOAD_HISTORY.filter((u) => u.kind === kind); -}