diff --git a/UPGRADE_PLAN.md b/UPGRADE_PLAN.md index 67580af..7ef96c3 100644 --- a/UPGRADE_PLAN.md +++ b/UPGRADE_PLAN.md @@ -1148,6 +1148,77 @@ model SLATarget { --- +### 9.5 — Weekly Executive Report + +**What:** A manually generated, print-ready weekly report summarizing production activity +for executive stakeholders. Covers deliverables completed, deadlines met/missed, pipeline +status, at-risk projects, and the upcoming week's schedule. Available as an in-browser +page and downloadable PDF. + +**Why:** Executives don't use the tracker daily but need a regular pulse on production +health. A structured, branded report replaces ad-hoc Slack summaries and gives leadership +consistent, reliable visibility into team output and project risk. + +**Report sections:** + +1. **Deliverables Completed** — count + list with project name, deliverable type, stage, + and completion date. The "wins" summary for the week. +2. **Deadlines Met vs. Missed** — ratio with trend vs. previous weeks (e.g., "14/16 met, + 87.5%"). Missed deadlines called out by name with project, days overdue, and current + status. +3. **Pipeline Snapshot** — aggregate status breakdown across all active projects: how many + stages are In Progress, In Review, Blocked, Approved. Stacked bar or donut chart. +4. **Projects At Risk** — projects with overdue deliverables, stages stuck too long, or + approaching deadlines with low completion rates. Auto-flagged based on configurable + thresholds. +5. **Upcoming Week** — deliverables due in the next 7 days, grouped by project. Gives + executives a heads-up on what to expect. +6. **Team Utilization Summary** (optional) — high-level capacity usage without individual + names (e.g., "Team at 82% capacity, 2 artists overallocated"). Added when Phase 6 + workload data is available. + +**Implementation:** + +- Report data service that accepts a date range (defaults to previous Monday–Sunday) and + aggregates: completed stages, missed deadlines, current status distribution, at-risk + projects, upcoming deadlines +- Dedicated report page at `/reports/weekly/[date]` — viewable in-browser with + print-optimized CSS and the Oliver Agency branded layout (forest green + coral palette, + Montserrat headings, `label-upper` section headers) +- PDF export via `@react-pdf/renderer` — same branded layout rendered as a downloadable + PDF from a "Download PDF" button on the report page +- "Generate Weekly Report" action accessible from the dashboard — opens the report page + for the selected week +- Date picker to generate reports for any past week, not just the current one +- All data sourced from existing models — no new database tables required + +**Future enhancements (not in initial scope):** +- Scheduled auto-generation via cron job (every Monday morning) +- Email distribution to a configured recipient list +- Customizable report sections per organization + +**No new data model required** — purely an aggregation and presentation layer over existing +`DeliverableStage`, `Project`, `Revision`, and `StageAssignment` records. + +**Key files:** +- `src/app/(app)/reports/weekly/[date]/page.tsx` — In-browser report page +- `src/components/reports/weekly/report-header.tsx` — Branded header with date range + org info +- `src/components/reports/weekly/completed-section.tsx` — Deliverables completed table +- `src/components/reports/weekly/deadlines-section.tsx` — Met/missed breakdown with trend +- `src/components/reports/weekly/pipeline-snapshot.tsx` — Status distribution chart +- `src/components/reports/weekly/at-risk-section.tsx` — Flagged projects list +- `src/components/reports/weekly/upcoming-section.tsx` — Next week's schedule +- `src/components/reports/weekly/report-pdf.tsx` — React-pdf document definition +- `src/lib/services/weekly-report-service.ts` — Data aggregation queries +- `src/hooks/use-weekly-report.ts` — TanStack Query hook + +**New dependency:** `@react-pdf/renderer` (PDF generation from React components) + +**Dependencies:** None — uses existing data. Enhanced by 6.1 (Capacity data for +utilization section) and 9.1 (Velocity metrics for trend calculations). + +--- + ## Phase 10: Collaboration Enhancements Deepens the communication layer to reduce dependency on email and Slack for @@ -1567,11 +1638,11 @@ npm run dev | 6 | `/api/workload/`, `/api/skills/`, `/api/users/[id]/skills/` | | 7 | `/api/automations/`, `/api/automations/[id]/executions/`, `/api/approval-chains/`, `/api/stages/[id]/approve/`, `/api/templates/`, `/api/templates/[id]/instantiate/` | | 8 | `/api/asset-specs/`, `/api/revisions/[id]/validate/`, `/api/webhooks/ai-review/`, `/api/revisions/[id]/ai-review/`, `/api/search/semantic/` | -| 9 | `/api/portal/`, `/api/portal/[token]/`, `/api/analytics/velocity/`, `/api/analytics/sla/` | +| 9 | `/api/portal/`, `/api/portal/[token]/`, `/api/analytics/velocity/`, `/api/analytics/sla/`, `/api/reports/weekly/` | | 10 | `/api/projects/[id]/activity/`, `/api/external-links/` | | 11 | `/api/views/` | -**Total new API routes: ~26** +**Total new API routes: ~27** --- @@ -1583,11 +1654,11 @@ npm run dev | 6 | Workload/capacity page, Skills management (settings) | | 7 | Automations management (settings), Approval chains (settings), Template library | | 8 | Asset specs (settings), Smart search panel (chat UI) | -| 9 | Client portal (external), SLA configuration (settings) | +| 9 | Client portal (external), SLA configuration (settings), Weekly executive report | | 10 | Activity feed (per project), External review page | | 11 | — (enhancements to existing pages) | -**Total new pages: ~13** +**Total new pages: ~14** --- @@ -1598,6 +1669,7 @@ npm run dev | `sharp` | Server-side image processing (validation, thumbnails, previews) | 8 | | `fabric.js` or native Canvas/SVG | Annotation drawing on canvas (evaluate during 5.2) | 5 | | `bcryptjs` | Password hashing for portal links | 9 | +| `@react-pdf/renderer` | PDF generation for weekly executive reports | 9 | **Philosophy:** Minimize new dependencies. Most features build on the existing stack (React, TanStack Query, shadcn/ui, recharts, Prisma). Canvas/SVG APIs are native browser @@ -1637,6 +1709,8 @@ Phase 8 (Asset Intelligence) ─── requires Phase 5 for full value +-- 8.4 Semantic Search <-- standalone, requires pgvector extension Phase 9 (Reporting) ─── benefits from Phase 6 + 7 data + | + +-- 9.5 Weekly Executive Report <-- standalone, no new models needed Phase 10 (Collaboration) ─── benefits from Phase 5 Phase 11 (QoL) ─── standalone incremental improvements, can be interleaved @@ -1658,14 +1732,14 @@ Phase 12 (Docker) ─── can be done at any time, benefits from 8.4 for Ollam | 6 | 3 | 3 | 2 | ~8 | | 7 | 6 | 6 | 3 | ~10 | | 8 | 4 | 5 | 2 | ~13 | -| 9 | 2 | 4 | 2 | ~8 | +| 9 | 2 | 5 | 3 | ~18 | | 10 | 1 | 2 | 2 | ~6 | | 11 | 1 | 1 | 0 | ~8 | | 12 | 0 | 0 | 0 | 0 (infra only) | -| **Total** | **22** | **~27** | **~14** | **~85** | +| **Total** | **22** | **~28** | **~15** | **~95** | --- -*Document version: 1.1 — Created 2026-03-01, updated 2026-03-06* -*Updates: Added 8.4 (AI semantic search with Ollama + pgvector), Phase 12 (Docker deployment)* +*Document version: 1.2 — Created 2026-03-01, updated 2026-03-13* +*Updates: Added 8.4 (AI semantic search with Ollama + pgvector), Phase 12 (Docker deployment), 9.5 (Weekly executive report)* *To be updated as features are refined and priorities shift.* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 14afcc8..1e802b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@hookform/resolvers": "^5.2.2", "@prisma/adapter-pg": "^7.4.2", "@prisma/client": "^7.4.2", + "@react-pdf/renderer": "^4.3.2", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.21", @@ -4564,6 +4565,180 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@react-pdf/fns": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz", + "integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.4.tgz", + "integrity": "sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^4.1.0", + "@react-pdf/types": "^2.9.2", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.4.tgz", + "integrity": "sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==", + "license": "MIT", + "dependencies": { + "@react-pdf/png-js": "^3.0.0", + "jay-peg": "^1.1.1" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.2.tgz", + "integrity": "sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/image": "^3.0.4", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.2", + "@react-pdf/textkit": "^6.1.0", + "@react-pdf/types": "^2.9.2", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.1.0.tgz", + "integrity": "sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/png-js": "^3.0.0", + "browserify-zlib": "^0.2.0", + "crypto-js": "^4.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "linebreak": "^1.1.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/png-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz", + "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==", + "license": "MIT", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz", + "integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz", + "integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/reconciler/node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/@react-pdf/render": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.2.tgz", + "integrity": "sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/textkit": "^6.1.0", + "@react-pdf/types": "^2.9.2", + "abs-svg-path": "^0.1.1", + "color-string": "^1.9.1", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.2.tgz", + "integrity": "sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.2", + "@react-pdf/font": "^4.0.4", + "@react-pdf/layout": "^4.4.2", + "@react-pdf/pdfkit": "^4.1.0", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/reconciler": "^2.0.0", + "@react-pdf/render": "^4.3.2", + "@react-pdf/types": "^2.9.2", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.2.tgz", + "integrity": "sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "@react-pdf/types": "^2.9.2", + "color-string": "^1.9.1", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.1.0.tgz", + "integrity": "sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.2", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.2.tgz", + "integrity": "sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.4", + "@react-pdf/primitives": "^4.1.1", + "@react-pdf/stylesheet": "^6.1.2" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -5740,6 +5915,12 @@ "win32" ] }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -6203,6 +6384,15 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -6277,6 +6467,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -6634,6 +6842,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6685,9 +6902,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -6789,6 +7015,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", @@ -7194,6 +7426,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7304,6 +7542,12 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -8034,6 +8278,15 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -8119,7 +8372,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -8249,6 +8501,23 @@ "dev": true, "license": "ISC" }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -8776,6 +9045,21 @@ "node": ">=16.9.0" } }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -8793,6 +9077,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/hyphen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", + "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", + "license": "ISC" + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -8981,6 +9271,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -9371,6 +9667,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -9449,6 +9751,15 @@ "node": ">= 0.4" } }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -9471,7 +9782,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -9958,6 +10268,25 @@ "node": ">=10" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -10094,7 +10423,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -10449,6 +10777,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11402,6 +11736,15 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/nuqs": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.9.tgz", @@ -11477,7 +11820,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11724,6 +12066,12 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11954,6 +12302,12 @@ "node": ">=4" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/postgres": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", @@ -12205,7 +12559,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -12217,7 +12570,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/proper-lockfile": { @@ -12289,6 +12641,15 @@ ], "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12850,6 +13211,15 @@ "url": "https://github.com/sponsors/remeda" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -12897,6 +13267,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -13347,6 +13723,15 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -13661,6 +14046,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -13730,6 +14121,12 @@ "node": ">=6" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -14080,6 +14477,32 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -14429,6 +14852,20 @@ "d3-timer": "^3.0.1" } }, + "node_modules/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14624,6 +15061,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zeptomatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", diff --git a/package.json b/package.json index 9432dd8..fe39dcd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@hookform/resolvers": "^5.2.2", "@prisma/adapter-pg": "^7.4.2", "@prisma/client": "^7.4.2", + "@react-pdf/renderer": "^4.3.2", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.21", diff --git a/src/app/(app)/reports/page.tsx b/src/app/(app)/reports/page.tsx new file mode 100644 index 0000000..c957c31 --- /dev/null +++ b/src/app/(app)/reports/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { format } from "date-fns"; + +/** + * /reports redirects to the current week's weekly report. + */ +export default function ReportsPage() { + const router = useRouter(); + + useEffect(() => { + const today = format(new Date(), "yyyy-MM-dd"); + router.replace(`/reports/weekly/${today}`); + }, [router]); + + return null; +} diff --git a/src/app/(app)/reports/weekly/[date]/page.tsx b/src/app/(app)/reports/weekly/[date]/page.tsx new file mode 100644 index 0000000..3df643c --- /dev/null +++ b/src/app/(app)/reports/weekly/[date]/page.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useParams, useRouter } from "next/navigation"; +import { format, parseISO, subWeeks, addWeeks, isValid } from "date-fns"; +import { useWeeklyReport } from "@/hooks/use-weekly-report"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ReportHeader } from "@/components/reports/weekly/report-header"; +import { KpiStrip } from "@/components/reports/weekly/kpi-strip"; +import { CompletedSection } from "@/components/reports/weekly/completed-section"; +import { DeadlinesSection } from "@/components/reports/weekly/deadlines-section"; +import { PipelineSnapshot } from "@/components/reports/weekly/pipeline-snapshot"; +import { AtRiskSection } from "@/components/reports/weekly/at-risk-section"; +import { UpcomingSection } from "@/components/reports/weekly/upcoming-section"; + +export default function WeeklyReportPage() { + const params = useParams<{ date: string }>(); + const router = useRouter(); + const dateStr = params.date; + + // Validate date param + const parsed = parseISO(dateStr); + const isValidDate = isValid(parsed); + + const { data, isLoading, error } = useWeeklyReport( + isValidDate ? dateStr : "" + ); + + function navigateWeek(direction: "prev" | "next") { + const current = parseISO(dateStr); + const target = + direction === "prev" ? subWeeks(current, 1) : addWeeks(current, 1); + router.push(`/reports/weekly/${format(target, "yyyy-MM-dd")}`); + } + + function handleDownloadPdf() { + // Stub — will be wired to @react-pdf/renderer + window.print(); + } + + if (!isValidDate) { + return ( +
+ Invalid date format. Use YYYY-MM-DD. +
++ Failed to load report. {error?.message} +
++ All projects on track +
++ No projects flagged for overdue deliverables or low completion +
++ {project.name} +
+ {project.projectCode && ( + + {project.projectCode} + + )} ++ {project.totalDeliverables} deliverable{project.totalDeliverables !== 1 ? "s" : ""} + {project.nearestDeadline && ( + <> + {" · Next deadline "} + {format(new Date(project.nearestDeadline), "MMM d")} + > + )} +
++ {project.overdueCount} +
+Overdue
++ {project.completionRate}% +
++ + {projects.length - 6} more at-risk project{projects.length - 6 !== 1 ? "s" : ""} +
+ )} ++ No stages completed this week +
+ ) : ( +| Deliverable | +Project | +Stage | ++ Completed + | +
|---|---|---|---|
| {item.name} | ++ {i === 0 ? projectName : ""} + | ++ + {item.stageName} + + | ++ {format(new Date(item.completedDate), "EEE, MMM d")} + | +
+ {met.length} +
+Met
++ {missed.length} +
+Missed
++ All deadlines met +
++ {total > 0 ? `${met.length} of ${total} on time` : "No deadlines this week"} +
+
+
{item.name}
++ {item.projectName} +
++ +{item.daysOverdue}d +
++ Due {format(new Date(item.dueDate), "MMM d")} +
++ + {missed.length - 8} more overdue deliverable{missed.length - 8 !== 1 ? "s" : ""} +
+{kpi.label}
++ {kpi.value} +
++ No active stages +
+ ) : ( ++ {weekLabel} +
++ Generated {generatedAt} +
++ No deliverables due next week +
+ ) : ( +| Due | +Deliverable | +Project | +Status | +Priority | +
|---|---|---|---|---|
| + {i === 0 + ? format(new Date(item.dueDate), "EEE, MMM d") + : ""} + | +{item.name} | ++ {item.projectName} + | +
+ |
+
+ |
+