feat(S04/T02): Made the job table show actionable follow-up/package nex…
- job-tracker-ui/src/components/JobTable.tsx - job-tracker-ui/src/daily-control-loop.test.tsx - job-tracker-ui/src/i18n/translations.ts - .gsd/milestones/M001/slices/S04/S04-PLAN.md - .gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md
This commit is contained in:
+16
-1
@@ -1 +1,16 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"id": "dec06736",
|
||||||
|
"label": "job-tracker-ui-build-serve",
|
||||||
|
"command": "npx serve -s job-tracker-ui/build -l 4173",
|
||||||
|
"cwd": "/home/pi/.gsd/projects/a40e97ae9e8f/worktrees/M001",
|
||||||
|
"ownerSessionFile": "/home/pi/.gsd/sessions/--home-pi-development-JobTracker--/2026-03-24T12-44-27-361Z_3ca4ee97-fc80-4cec-b8e7-f5b393c64480.jsonl",
|
||||||
|
"persistAcrossSessions": false,
|
||||||
|
"startedAt": 1774356678346,
|
||||||
|
"processType": "server",
|
||||||
|
"group": null,
|
||||||
|
"readyPattern": "Accepting connections|Local:|http://localhost:4173",
|
||||||
|
"readyPort": 4173,
|
||||||
|
"pid": 1141701
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -5,3 +5,4 @@
|
|||||||
- The S02 package workspace persists the application-answer draft inside `JobApplication.Notes` using the marker block `<<<APPLICATION_ANSWER_DRAFT>>> ... <<<END_APPLICATION_ANSWER_DRAFT>>>`; downstream slices should replace or parse that block instead of appending free-form notes.
|
- The S02 package workspace persists the application-answer draft inside `JobApplication.Notes` using the marker block `<<<APPLICATION_ANSWER_DRAFT>>> ... <<<END_APPLICATION_ANSWER_DRAFT>>>`; downstream slices should replace or parse that block instead of appending free-form notes.
|
||||||
- `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobApplicationsApplicationPackageTests` is now a trustworthy direct verification command in this worktree for package-generation and notes-replacement behavior; prefer it over older isolated-harness guidance when checking S02 regressions.
|
- `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobApplicationsApplicationPackageTests` is now a trustworthy direct verification command in this worktree for package-generation and notes-replacement behavior; prefer it over older isolated-harness guidance when checking S02 regressions.
|
||||||
- `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobApplicationsFollowUpDraftTests` is now trustworthy again in this worktree after restoring the missing ASP.NET Core / Identity / xUnit test-project references in `JobTrackerApi.Tests/JobTrackerApi.Tests.csproj`; older task notes that require an isolated Docker harness are stale.
|
- `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobApplicationsFollowUpDraftTests` is now trustworthy again in this worktree after restoring the missing ASP.NET Core / Identity / xUnit test-project references in `JobTrackerApi.Tests/JobTrackerApi.Tests.csproj`; older task notes that require an isolated Docker harness are stale.
|
||||||
|
- Running `npm --prefix job-tracker-ui start` alone is not enough for browser UAT in this worktree: the frontend calls `http://localhost:5202/api/...`, so without the backend (or a matching CORS/proxy setup) the UI loads but shows empty-state surfaces with `net::ERR_FAILED`/CORS errors instead of real job data.
|
||||||
|
|||||||
@@ -43,3 +43,20 @@
|
|||||||
{"ts":"2026-03-24T11:32:22.159Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":2,"eventType":"dispatch-match","rule":"summarizing → complete-slice","data":{"unitType":"complete-slice","unitId":"M001/S03"}}
|
{"ts":"2026-03-24T11:32:22.159Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":2,"eventType":"dispatch-match","rule":"summarizing → complete-slice","data":{"unitType":"complete-slice","unitId":"M001/S03"}}
|
||||||
{"ts":"2026-03-24T11:32:22.165Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":3,"eventType":"unit-start","data":{"unitType":"complete-slice","unitId":"M001/S03"}}
|
{"ts":"2026-03-24T11:32:22.165Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":3,"eventType":"unit-start","data":{"unitType":"complete-slice","unitId":"M001/S03"}}
|
||||||
{"ts":"2026-03-24T11:40:09.423Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":4,"eventType":"unit-end","data":{"unitType":"complete-slice","unitId":"M001/S03","status":"completed","artifactVerified":true},"causedBy":{"flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":3}}
|
{"ts":"2026-03-24T11:40:09.423Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":4,"eventType":"unit-end","data":{"unitType":"complete-slice","unitId":"M001/S03","status":"completed","artifactVerified":true},"causedBy":{"flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":3}}
|
||||||
|
{"ts":"2026-03-24T11:40:09.882Z","flowId":"adff88bd-a004-44ee-8707-a92cf11b993c","seq":5,"eventType":"iteration-end","data":{"iteration":6}}
|
||||||
|
{"ts":"2026-03-24T11:40:09.883Z","flowId":"eee2e6cb-cc05-4b58-b4f2-6471f9fbadd3","seq":1,"eventType":"iteration-start","data":{"iteration":7}}
|
||||||
|
{"ts":"2026-03-24T11:40:09.976Z","flowId":"eee2e6cb-cc05-4b58-b4f2-6471f9fbadd3","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T11:40:09.984Z","flowId":"eee2e6cb-cc05-4b58-b4f2-6471f9fbadd3","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T11:55:32.899Z","flowId":"f415178f-7473-42c6-855f-3197cfdbc267","seq":1,"eventType":"iteration-start","data":{"iteration":1}}
|
||||||
|
{"ts":"2026-03-24T11:55:33.001Z","flowId":"f415178f-7473-42c6-855f-3197cfdbc267","seq":2,"eventType":"dispatch-match","rule":"summarizing → complete-slice","data":{"unitType":"complete-slice","unitId":"M001/S04"}}
|
||||||
|
{"ts":"2026-03-24T11:55:33.009Z","flowId":"f415178f-7473-42c6-855f-3197cfdbc267","seq":3,"eventType":"unit-start","data":{"unitType":"complete-slice","unitId":"M001/S04"}}
|
||||||
|
{"ts":"2026-03-24T12:11:33.612Z","flowId":"f7f3f549-4db8-4bd4-8d39-84fe0fbac7c6","seq":1,"eventType":"iteration-start","data":{"iteration":1}}
|
||||||
|
{"ts":"2026-03-24T12:11:33.689Z","flowId":"f7f3f549-4db8-4bd4-8d39-84fe0fbac7c6","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:11:33.696Z","flowId":"f7f3f549-4db8-4bd4-8d39-84fe0fbac7c6","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:26:34.819Z","flowId":"a3d26ff3-c20d-49a3-8be6-e8c956e6ab6f","seq":1,"eventType":"iteration-start","data":{"iteration":1}}
|
||||||
|
{"ts":"2026-03-24T12:26:34.919Z","flowId":"a3d26ff3-c20d-49a3-8be6-e8c956e6ab6f","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:26:34.926Z","flowId":"a3d26ff3-c20d-49a3-8be6-e8c956e6ab6f","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:44:27.227Z","flowId":"bc366480-21fd-4628-a971-365a5dbeff56","seq":1,"eventType":"iteration-start","data":{"iteration":1}}
|
||||||
|
{"ts":"2026-03-24T12:44:27.343Z","flowId":"bc366480-21fd-4628-a971-365a5dbeff56","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:44:27.348Z","flowId":"bc366480-21fd-4628-a971-365a5dbeff56","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S04/T02"}}
|
||||||
|
{"ts":"2026-03-24T12:56:42.382Z","flowId":"bc366480-21fd-4628-a971-365a5dbeff56","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S04/T02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"bc366480-21fd-4628-a971-365a5dbeff56","seq":3}}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ S04 directly owns active requirements **R005**, **R006**, **R007**, **R009**, an
|
|||||||
- `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx`
|
- `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx`
|
||||||
- `CI=true npm --prefix job-tracker-ui run build`
|
- `CI=true npm --prefix job-tracker-ui run build`
|
||||||
- Manual UAT: from the table, reminders page, and dashboard, identify a job needing attention, jump into the correct job workspace tab, and confirm the flow feels like one coherent daily review loop.
|
- Manual UAT: from the table, reminders page, and dashboard, identify a job needing attention, jump into the correct job workspace tab, and confirm the flow feels like one coherent daily review loop.
|
||||||
|
- Failure-path check: deliberately trigger each surface's action affordance in the focused UI test and confirm the routed workspace lands on the expected job/tab state instead of leaving the user on a passive summary surface.
|
||||||
|
|
||||||
## Observability / Diagnostics
|
## Observability / Diagnostics
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ S04 directly owns active requirements **R005**, **R006**, **R007**, **R009**, an
|
|||||||
- Do: add actionable attention cards/lists to the dashboard, route reminders actions into the existing job workspace state instead of a separate modal loop, and preserve the individual-first control flow through `/jobs`.
|
- Do: add actionable attention cards/lists to the dashboard, route reminders actions into the existing job workspace state instead of a separate modal loop, and preserve the individual-first control flow through `/jobs`.
|
||||||
- Verify: `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx`
|
- Verify: `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx`
|
||||||
- Done when: the dashboard and reminders surfaces clearly show what needs attention and can open the correct job workspace state directly.
|
- Done when: the dashboard and reminders surfaces clearly show what needs attention and can open the correct job workspace state directly.
|
||||||
- [ ] **T02: Make the job table expose the right next action and prove the daily loop** `est:4h`
|
- [x] **T02: Make the job table expose the right next action and prove the daily loop** `est:4h`
|
||||||
- Why: The table is the first surface the user sees each day, so it has to make urgency legible and connect cleanly into the same routed job workspace flow.
|
- Why: The table is the first surface the user sees each day, so it has to make urgency legible and connect cleanly into the same routed job workspace flow.
|
||||||
- Files: `job-tracker-ui/src/components/JobTable.tsx`, `job-tracker-ui/src/daily-control-loop.test.tsx`
|
- Files: `job-tracker-ui/src/components/JobTable.tsx`, `job-tracker-ui/src/daily-control-loop.test.tsx`
|
||||||
- Do: tighten job-table urgency/action affordances for follow-up and package work, reuse the routed workspace-open mechanism, and add focused UI coverage proving the table/reminders/dashboard loop lands in the right job workspace state.
|
- Do: tighten job-table urgency/action affordances for follow-up and package work, reuse the routed workspace-open mechanism, and add focused UI coverage proving the table/reminders/dashboard loop lands in the right job workspace state.
|
||||||
|
|||||||
@@ -42,3 +42,9 @@ Strengthen the job table so the first daily view shows what action is actually d
|
|||||||
|
|
||||||
- `job-tracker-ui/src/components/JobTable.tsx` — clearer next-action affordances.
|
- `job-tracker-ui/src/components/JobTable.tsx` — clearer next-action affordances.
|
||||||
- `job-tracker-ui/src/daily-control-loop.test.tsx` — proof that the table participates in the routed daily loop.
|
- `job-tracker-ui/src/daily-control-loop.test.tsx` — proof that the table participates in the routed daily loop.
|
||||||
|
|
||||||
|
## Observability Impact
|
||||||
|
|
||||||
|
- Signals changed: the table's urgency chips and primary row actions should now expose the same routed follow-up and package-work intents already used by dashboard and reminders.
|
||||||
|
- How to inspect later: read `job-tracker-ui/src/components/JobTable.tsx` for the shared workspace-route usage and run `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx` to confirm table, reminders, and dashboard all land in the expected workspace state.
|
||||||
|
- Failure state made visible: if table actions drift from the shared routing contract, the focused UI test should fail with the wrong route/tab content instead of silently leaving decorative chips in place.
|
||||||
|
|||||||
@@ -1,23 +1,89 @@
|
|||||||
---
|
---
|
||||||
title: T02 summary
|
id: T02
|
||||||
status: done
|
parent: S04
|
||||||
files:
|
milestone: M001
|
||||||
|
provides:
|
||||||
|
- Makes job-table urgency signals actionable and keeps table, reminders, and dashboard on the same routed job-workspace loop.
|
||||||
|
key_files:
|
||||||
- job-tracker-ui/src/components/JobTable.tsx
|
- job-tracker-ui/src/components/JobTable.tsx
|
||||||
- job-tracker-ui/src/daily-control-loop.test.tsx
|
- job-tracker-ui/src/daily-control-loop.test.tsx
|
||||||
verification:
|
- job-tracker-ui/src/i18n/translations.ts
|
||||||
- CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx
|
- .gsd/milestones/M001/slices/S04/S04-PLAN.md
|
||||||
- CI=true npm --prefix job-tracker-ui run build
|
- .gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md
|
||||||
|
key_decisions:
|
||||||
|
- Derived table chips and primary row actions from one shared action-signal model so table urgency and routed next-action behavior cannot drift.
|
||||||
|
patterns_established:
|
||||||
|
- Use the shared `/jobs?open=...&tab=...&followMode=...` workspace route from overview surfaces and prove each surface with focused UI/browser checks.
|
||||||
|
observability_surfaces:
|
||||||
|
- job-tracker-ui/src/components/JobTable.tsx
|
||||||
|
- job-tracker-ui/src/daily-control-loop.test.tsx
|
||||||
|
- Browser UAT on the built app served locally with mocked API routes at http://localhost:4173
|
||||||
|
- .gsd/milestones/M001/slices/S04/S04-PLAN.md verification/failure-path notes
|
||||||
|
duration: ~2h
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-24T13:55:43+01:00
|
||||||
|
blocker_discovered: false
|
||||||
---
|
---
|
||||||
|
|
||||||
Made the job table expose the next action directly and prove the daily loop end-to-end.
|
# T02: Make the job table expose the right next action and prove the daily loop
|
||||||
|
|
||||||
What changed:
|
**Made the job table show actionable follow-up/package next steps and proved table, reminders, and dashboard all land in the same job workspace flow.**
|
||||||
- `job-tracker-ui/src/components/JobTable.tsx`
|
|
||||||
- follow-up and CV status chips now route directly into the right job workspace tab
|
|
||||||
- the table participates in the same routed workspace flow as dashboard and reminders
|
|
||||||
- `job-tracker-ui/src/daily-control-loop.test.tsx`
|
|
||||||
- expanded the focused test to prove the table chip routes into the follow-up workspace and that reminders can route into the tailored-CV workspace
|
|
||||||
|
|
||||||
Verification:
|
## What Happened
|
||||||
- Focused daily-loop test passed
|
|
||||||
- Frontend build passed
|
I first fixed the flagged observability gaps in the slice and task plans so this unit explicitly described its failure-path coverage and inspection surfaces.
|
||||||
|
|
||||||
|
In `job-tracker-ui/src/components/JobTable.tsx`, I replaced the split decorative/action logic with a shared action-signal model. The table now derives both row chips and the primary “Next action” control from the same follow-up/package readiness signals, so the visible urgency affordance and the routed action cannot drift apart. Package work now reflects the same readiness inputs already used by the table filters: missing tailored CV, missing notes, or both.
|
||||||
|
|
||||||
|
I updated translations in `job-tracker-ui/src/i18n/translations.ts` so the new package-work affordances and details are user-facing copy instead of hardcoded strings.
|
||||||
|
|
||||||
|
In `job-tracker-ui/src/daily-control-loop.test.tsx`, I expanded the focused daily-loop proof so the table explicitly participates in the routed loop: the test now clicks a table urgency signal for follow-up work and the table’s next-action button for package work, while the earlier reminders/dashboard proofs remain in place.
|
||||||
|
|
||||||
|
For runtime verification, I also exercised the built app in a browser. Because `dotnet` is unavailable in this environment, I served the compiled UI locally and mocked the frontend’s own API contract in-browser, then verified that table, reminders, and dashboard each opened the expected job workspace state.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Verified the focused UI test passes, the frontend production build succeeds, and browser UAT confirms the routed daily loop across all three surfaces.
|
||||||
|
|
||||||
|
- Focused test: `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx`
|
||||||
|
- Build: `CI=true npm --prefix job-tracker-ui run build`
|
||||||
|
- Browser UAT on built app (`npx serve -s job-tracker-ui/build -l 4173`) with mocked API contract:
|
||||||
|
- `/jobs`: clicked table next action for Backend Developer and confirmed Follow-up context + saved cover-letter reuse signal
|
||||||
|
- `/reminders`: clicked Missing tailored CV → Open and confirmed Tailored CV workspace for Platform Engineer
|
||||||
|
- `/dashboard`: clicked Follow up card action and confirmed routed follow-up workspace
|
||||||
|
- Network errors were cleared after route mocking; the verified browser interactions completed without failed mocked requests
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx` | 0 | ✅ pass | 4.723s |
|
||||||
|
| 2 | `CI=true npm --prefix job-tracker-ui run build` | 0 | ✅ pass | 12.969s |
|
||||||
|
| 3 | Browser UAT on `http://localhost:4173` with mocked `http://localhost:5202/api/*` routes: table → follow-up workspace, reminders → tailored CV workspace, dashboard → follow-up workspace | 0 | ✅ pass | ~4m |
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
Inspect `job-tracker-ui/src/components/JobTable.tsx` to see the shared action-signal derivation that drives both urgency chips and primary next-action buttons.
|
||||||
|
|
||||||
|
Run `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/daily-control-loop.test.tsx` to catch drift between table, reminders, dashboard, and the routed workspace state.
|
||||||
|
|
||||||
|
For browser-level inspection, serve the built UI and verify these surfaces route into `/jobs` with the expected workspace content:
|
||||||
|
- table signal / next-action controls
|
||||||
|
- reminders Open actions
|
||||||
|
- dashboard Needs Follow-up actions
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Used browser-level API route mocks for manual UAT because the local ASP.NET API could not be started in this environment (`dotnet` is not installed). The shipped UI code was still exercised in a real browser against its own HTTP contract.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
The focused Jest run still emits existing React Router v7 future-flag warnings from the test harness. They do not fail the task’s verification gate and were not introduced by this change.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `job-tracker-ui/src/components/JobTable.tsx` — unified actionable urgency chips and primary next-action routing for follow-up and package work
|
||||||
|
- `job-tracker-ui/src/daily-control-loop.test.tsx` — extended the focused loop test to prove the table participates in the same routed workspace flow
|
||||||
|
- `job-tracker-ui/src/i18n/translations.ts` — added user-facing copy for package-work affordances and details
|
||||||
|
- `.gsd/milestones/M001/slices/S04/S04-PLAN.md` — added explicit slice-level failure-path verification guidance
|
||||||
|
- `.gsd/milestones/M001/slices/S04/tasks/T02-PLAN.md` — added the missing Observability Impact section
|
||||||
|
|||||||
@@ -289,6 +289,59 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
return src.length > 220 ? `${src.slice(0, 220)}...` : src;
|
return src.length > 220 ? `${src.slice(0, 220)}...` : src;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openFollowUpWorkspace = (jobId: number) => {
|
||||||
|
navigate(buildJobWorkspacePath(jobId, { tab: JOB_DETAILS_TABS.followUp, followMode: "waiting-update" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTailoredCvWorkspace = (jobId: number) => {
|
||||||
|
navigate(buildJobWorkspacePath(jobId, { tab: JOB_DETAILS_TABS.tailoredCv }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPackageActionDetail = (job: JobApplication) => {
|
||||||
|
const missingTailoredCv = !job.tailoredCvText;
|
||||||
|
const missingNotes = !job.notes?.trim();
|
||||||
|
|
||||||
|
if (missingTailoredCv && missingNotes) return t("jobTablePackageMissingCvAndNotes");
|
||||||
|
if (missingTailoredCv) return t("jobTableCvMissing");
|
||||||
|
if (missingNotes) return t("jobTablePackageMissingNotes");
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionSignals = (job: JobApplication) => {
|
||||||
|
const signals: Array<{
|
||||||
|
label: string;
|
||||||
|
detail: string;
|
||||||
|
onClick: () => void;
|
||||||
|
variant: "contained" | "outlined";
|
||||||
|
color?: "warning" | "primary";
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (job.needsFollowUp) {
|
||||||
|
signals.push({
|
||||||
|
label: t("jobTableFollowUp"),
|
||||||
|
detail: job.followUpReason ?? t("jobTableNeedsFollowUp"),
|
||||||
|
onClick: () => openFollowUpWorkspace(job.id),
|
||||||
|
variant: "contained",
|
||||||
|
color: "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageDetail = !job.isDeleted ? getPackageActionDetail(job) : null;
|
||||||
|
if (packageDetail) {
|
||||||
|
signals.push({
|
||||||
|
label: t("jobTablePackageWork"),
|
||||||
|
detail: packageDetail,
|
||||||
|
onClick: () => openTailoredCvWorkspace(job.id),
|
||||||
|
variant: job.needsFollowUp ? "outlined" : "contained",
|
||||||
|
color: job.needsFollowUp ? "primary" : "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signals;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrimaryAction = (job: JobApplication) => getActionSignals(job)[0] ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
||||||
@@ -368,6 +421,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
const open = expanded.includes(job.id);
|
const open = expanded.includes(job.id);
|
||||||
const toneName = statusTone(job.status);
|
const toneName = statusTone(job.status);
|
||||||
const tone = toneName === "error" ? theme.palette.error.main : toneName === "warning" ? theme.palette.warning.main : toneName === "success" ? theme.palette.success.main : toneName === "info" ? theme.palette.info.main : theme.palette.primary.main;
|
const tone = toneName === "error" ? theme.palette.error.main : toneName === "warning" ? theme.palette.warning.main : toneName === "success" ? theme.palette.success.main : toneName === "info" ? theme.palette.info.main : theme.palette.primary.main;
|
||||||
|
const primaryAction = getPrimaryAction(job);
|
||||||
|
const actionSignals = getActionSignals(job);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={job.id}>
|
<React.Fragment key={job.id}>
|
||||||
<TableRow sx={{ backgroundColor: alpha(tone, theme.palette.mode === "dark" ? 0.1 : 0.06) }}>
|
<TableRow sx={{ backgroundColor: alpha(tone, theme.palette.mode === "dark" ? 0.1 : 0.06) }}>
|
||||||
@@ -377,9 +432,20 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||||
<span>{job.jobTitle}</span>
|
<span>{job.jobTitle}</span>
|
||||||
{job.needsFollowUp ? <Chip size="small" label={t("jobTableFollowUp")} title={job.followUpReason ?? undefined} sx={{ fontWeight: 800, cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.followUp, followMode: "waiting-update" }))} /> : null}
|
{actionSignals.map((signal) => (
|
||||||
{!job.tailoredCvText && !job.isDeleted ? <Chip size="small" label={t("jobTableCvMissing")} color="warning" variant="outlined" sx={{ cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.tailoredCv }))} /> : null}
|
<Chip
|
||||||
{job.tailoredCvText ? <Chip size="small" label={t("jobTableCvReady")} color="success" variant="outlined" sx={{ cursor: "pointer" }} onClick={() => navigate(buildJobWorkspacePath(job.id, { tab: JOB_DETAILS_TABS.tailoredCv }))} /> : null}
|
key={`${job.id}-${signal.label}-${signal.detail}`}
|
||||||
|
size="small"
|
||||||
|
label={signal.label}
|
||||||
|
color={signal.color}
|
||||||
|
variant={signal.variant === "contained" ? "filled" : "outlined"}
|
||||||
|
title={signal.detail}
|
||||||
|
sx={{ fontWeight: 800, cursor: "pointer" }}
|
||||||
|
clickable
|
||||||
|
onClick={signal.onClick}
|
||||||
|
aria-label={`${job.jobTitle} — ${signal.label} signal`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{columns.status ? <TableCell><Chip label={normalizeStatus(job.status)} size="small" color={toneName as any} /></TableCell> : null}
|
{columns.status ? <TableCell><Chip label={normalizeStatus(job.status)} size="small" color={toneName as any} /></TableCell> : null}
|
||||||
@@ -387,12 +453,27 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
{columns.daysSince ? <TableCell>{job.daysSince}</TableCell> : null}
|
{columns.daysSince ? <TableCell>{job.daysSince}</TableCell> : null}
|
||||||
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableLink")}</a> : ""}</TableCell> : null}
|
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableLink")}</a> : ""}</TableCell> : null}
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 0.75 }}>
|
||||||
|
{primaryAction ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="caption" sx={{ color: "text.secondary", fontWeight: 700 }}>
|
||||||
|
{t("editJobNextAction")}
|
||||||
|
</Typography>
|
||||||
|
<Button size="small" variant={primaryAction.variant} color={primaryAction.color} onClick={primaryAction.onClick} aria-label={`${t("editJobNextAction")}: ${job.jobTitle} — ${primaryAction.label}`}>
|
||||||
|
{primaryAction.label}
|
||||||
|
</Button>
|
||||||
|
<Typography variant="caption" sx={{ color: "text.secondary", maxWidth: 220, textAlign: "right" }}>
|
||||||
|
{primaryAction.detail}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5 }}>
|
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5 }}>
|
||||||
<Tooltip title={t("jobTableEdit")}><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
<Tooltip title={t("jobTableEdit")}><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
<Tooltip title={t("jobTableOpen")}><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
<Tooltip title={t("jobTableOpen")}><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
||||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -160,13 +160,25 @@ test('reminders open action routes tailored-cv gaps into the tailored cv workspa
|
|||||||
expect(await screen.findByText(/build the package here, then save the working copy back onto this job/i)).toBeInTheDocument();
|
expect(await screen.findByText(/build the package here, then save the working copy back onto this job/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('job table urgency chips route into the correct workspace tab', async () => {
|
test('job table urgency signals and next actions route into the shared workspace flow', async () => {
|
||||||
renderLoop('/jobs');
|
const firstRender = renderLoop('/jobs');
|
||||||
|
|
||||||
const followUpChip = await screen.findByText(/follow up/i);
|
fireEvent.click(await screen.findByRole('button', { name: /backend developer — follow up signal/i }));
|
||||||
fireEvent.click(followUpChip);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText(/follow-up context/i)).toBeInTheDocument();
|
expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs');
|
||||||
});
|
});
|
||||||
|
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
firstRender.unmount();
|
||||||
|
renderLoop('/jobs');
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: /next action: platform engineer — build package/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs');
|
||||||
|
});
|
||||||
|
expect(await screen.findByText(/build the package here, then save the working copy back onto this job/i)).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(/platform work/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -616,6 +616,9 @@ export const translations = {
|
|||||||
jobTableFollowUp: "Follow up",
|
jobTableFollowUp: "Follow up",
|
||||||
jobTableCvMissing: "CV missing",
|
jobTableCvMissing: "CV missing",
|
||||||
jobTableCvReady: "CV ready",
|
jobTableCvReady: "CV ready",
|
||||||
|
jobTablePackageWork: "Build package",
|
||||||
|
jobTablePackageMissingCvAndNotes: "Tailored CV and job notes still need a working draft.",
|
||||||
|
jobTablePackageMissingNotes: "Job notes still need a working draft.",
|
||||||
jobTableOpenListing: "Open listing",
|
jobTableOpenListing: "Open listing",
|
||||||
jobTableSkills: "Skills",
|
jobTableSkills: "Skills",
|
||||||
jobTableNoTags: "No tags",
|
jobTableNoTags: "No tags",
|
||||||
@@ -1424,6 +1427,9 @@ export const translations = {
|
|||||||
jobTableFollowUp: "Følg opp",
|
jobTableFollowUp: "Følg opp",
|
||||||
jobTableCvMissing: "CV mangler",
|
jobTableCvMissing: "CV mangler",
|
||||||
jobTableCvReady: "CV klar",
|
jobTableCvReady: "CV klar",
|
||||||
|
jobTablePackageWork: "Bygg pakke",
|
||||||
|
jobTablePackageMissingCvAndNotes: "Tilpasset CV og jobbnotater trenger fortsatt et arbeidsutkast.",
|
||||||
|
jobTablePackageMissingNotes: "Jobbnotater trenger fortsatt et arbeidsutkast.",
|
||||||
jobTableOpenListing: "Åpne stilling",
|
jobTableOpenListing: "Åpne stilling",
|
||||||
jobTableSkills: "Ferdigheter",
|
jobTableSkills: "Ferdigheter",
|
||||||
jobTableNoTags: "Ingen tagger",
|
jobTableNoTags: "Ingen tagger",
|
||||||
|
|||||||
Reference in New Issue
Block a user