# Plan 7 — Profiles, Skill Catalog & AI Dispatch Context > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`). Follow the execution protocol (`2026-05-29-execution-protocol.md`) — sequential on shared files, parallel only where lanes are disjoint. **Goal:** Give every person a **two-layer profile** (self-edited + company-private), an **org skill catalog** with per-person 0–100 scores, surface **ProCanvas achievements**, and **ground LynkDispatch** — remap the fake `DISPATCH_REPS` to canonical people and rewrite each recommendation's `reasons`/`aiInsight`/`skillMatch` to **cite real profile facts** (equipment + skill scores), so "the AI read their profile" is demonstrably true on screen. **Architecture:** Add a `profile` object (`self` / `company` / `achievements`) to the canonical people in `mockStore.jsx`, plus an org-level `SKILL_CATALOG` and per-person `skillScores`, exposed via the store (a `resolveProfile(id)` selector + `updateOwnProfile`/`setSkillScore`/`addCatalogSkill` mutators). Then remap dispatch reps to canonical field agents and rewrite the pre-authored recommendations to reference those people's real profile facts. UI: a self-service **User Details** page (all non-customer roles edit their `self` layer), an owner **Person Details** view (sees `self` + `company` + skill scores + achievements, edits scores/notes), and a **Skill Catalog editor** in Org Settings. Plan 8 (ABAC) later governs who can see the `company` layer; Plan 7 gates it with a simple role check. **Tech stack:** React 18, Vite 7, mock React-context store, pnpm. No test harness → verify via `npm run build` + grep + dev-run (a dispatch recommendation's rationale must quote a real skill/equipment value from the cited rep's profile). **Spec/master:** `docs/superpowers/2026-05-29-MASTER-PLAN.md` §13 (profiles/skills/AI dispatch), Appendix A.3 (person & profile), A.7 (dispatch), Appendix G Plan 7. Builds on the canonical roster (Plan 1). --- ## Current state (verified) - **People** split across: `MOCK_USERS` (login; field agents e1–e5 carry `xp`/`streakDays`/`achievements[]`), `MOCK_PERSONNEL` (p1 Jesus, p2 Sarah — `sensitiveData`/`compliance`), `MOCK_SUBCONTRACTORS` (sub_001–005 — `tradeType`/`rating`), `MOCK_OWNERS` (company dashboards). **No `profile`, `skillScores`, `bio`, `equipment`, `pronouns`, `nickname` on anyone; no `SKILL_CATALOG`.** - **Self-edit page:** only `CustomerProfile.jsx` (`/portal/profile`, CUSTOMER only) edits name/email/phone/address via `updateProfile()` in `AuthContext`. **No User Details page for employee/owner/admin/contractor/subcontractor.** - **Owner person view:** `PeopleDirectory.jsx` (`/owner/people`) lists only `personnel` (p1/p2) + `vendors` — **field agents e1–e5 and subs sub_001–005 don't appear**. Detail panel shows email/phone/masked SSN/compliance; "Edit Profile"/"Request Document" are **stubs**. No skill scores / internal notes anywhere. `VendorDirectory.jsx` shows `MOCK_VENDORS` (v1–v6, separate from subs). - **Gamification:** `GamificationContext.jsx` is a single **global** `INITIAL_STATE` (xp 3360, level 12, streak 4 …) — NOT per-person. Field agents' `xp`/`streakDays`/`achievements` in MOCK_USERS are static + disconnected. `LEADERBOARD_MOCK` has fake names (Alex M., Jessica R., …). - **LynkDispatch:** `DISPATCH_REPS` (mockStore ~line 4645) = **REP-01…05 with FAKE names** (Carlos Mendez, Aisha Kumar, Tom Bradley, Nina Patel, Derek Walsh) — **zero link to canonical people**. `DISPATCH_RECOMMENDATIONS` (~4842) have `{repId, score, slotStart/End, distanceMi, factors{proximity,driveTime,calendarFit,weather,skillMatch}, reasons[], stormReasons[], aiInsight, conflictFlag, routePath}` — rendered fully on screen (`DispatchRecommendationDrawer.jsx` shows factor bars + aiInsight + reasons chips). `skillMatch` is a static integer, not derived. `DISPATCH_LEADS_INITIAL` has fake `canvassedBy: {agentId:'U-05', name:'Marcus Webb'}` / `'U-07' Priya Nair`. `DISPATCH_WEEKLY_STATS.repBreakdown` keyed by REP-##. - **Skills:** only `DISPATCH_REPS[].skills = ['insurance_claim','retail_estimate',…]` (plain strings). No catalog, no scores. - **Routes:** no `/profile`/`/settings`/`/user-details` for non-customer roles; room for a universal profile route and a `/owner/people/:personId` detail. --- ## Schemas (target — per Appendix A.3) Org-level skill catalog (in `mockStore.jsx`): ```js export const SKILL_CATALOG = [ { id: 'sk_insurance_claim', name: 'Insurance Claim Handling', description: 'Files & negotiates carrier claims/supplements.' }, { id: 'sk_roof_inspection', name: 'Roof Inspection', description: 'Damage assessment, measurements, documentation.' }, { id: 'sk_emergency_tarp', name: 'Emergency Tarp', description: 'Rapid storm tarp / water mitigation.' }, { id: 'sk_retail_estimate', name: 'Retail Estimate', description: 'Cash/retail bid preparation & close.' }, { id: 'sk_electrical', name: 'Electrical', description: 'Panel/wiring/EV per NEC.' }, { id: 'sk_drone_survey', name: 'Drone Survey', description: 'Aerial roof imaging & measurement.' }, { id: 'sk_customer_comms', name: 'Customer Communication', description: 'Homeowner rapport & expectation-setting.' }, ]; ``` Per-person `profile` (added to the canonical people): ```js profile: { self: { // person edits; owner can view preferredName, photo, phone, altContact, address, emergencyContact, nickname, pronouns, gender, sex, // all optional bio, yearsExperience, languages: [], specialties: [], equipment: [], availability, serviceAreas: [], // subcontractors also: license, insurance: { coi, expiry }, w9Status, }, company: { // owner/admin only internalNotes, statusTags: [], // 'preferred' | 'probation' | 'do-not-use' | ... skillScores: [{ skillId, score }], // score = 0..100 or 'N/A' payTerms, hireDate, managerId, }, achievements: { level, xp, streak, badges: [], tiers: [] }, // employees; from MOCK_USERS gamification } ``` --- ## Lane / ownership map | Task | Lane (exclusive files) | Phase | |------|------------------------|-------| | T1 SKILL_CATALOG + profiles + store API | `src/data/mockStore.jsx` | A (seq) | | T2 Dispatch remap + ground recommendations | `src/data/mockStore.jsx` | A (seq, after T1) | | T3 Self-service User Details page | new `src/pages/UserDetailsPage.jsx`, `src/App.jsx` | B (parallel) | | T4 Owner Person Details view | `src/pages/owner/PeopleDirectory.jsx` | B (parallel) | | T5 Skill Catalog editor + leaderboard names | `src/pages/owner/OrgSettings.jsx`, `src/context/GamificationContext.jsx` | B (parallel) | | T6 Verify | (read-only) | C | **Phase A (T1, T2) sequential** — both edit `mockStore.jsx` (T2's dispatch grounding cites T1's profile data). **Phase B (T3, T4, T5) parallel** — disjoint files (T3 owns App.jsx + the new page). **T6** last. --- ## Task 1: Skill catalog + profiles + store API **Files:** `src/data/mockStore.jsx` only. - [ ] **Step 1:** Add the `SKILL_CATALOG` constant (above) near the other exported constants. Expose it on the store context value as `skillCatalog`. - [ ] **Step 2:** Add a `profile` object (per the schema above) to each canonical person who needs one — at minimum the **5 field agents (e1–e5)**, the **2 reps (p1, p2)**, and the **5 subcontractors (sub_001–005)**; owners (own_001/002) and admins (a1–a3) get a lighter `self`+`company` (no skillScores required but allowed). Author realistic values: bios, `yearsExperience`, `languages`, `equipment` (e.g. Cody Tatum: `['ladder rack','drone','tarp kit']`; Carlos Mendoza: `['tarp','panel tester','bucket truck']`), `serviceAreas` (Plano zips), and `company.skillScores` referencing `SKILL_CATALOG` ids with 0–100 (or `'N/A'`) — make the scores tell a story (e.g. Cody 92 emergency_tarp + 88 insurance_claim; Maya Patel high roof_inspection). Subs get `license`/`insurance`/`w9Status`. Add `company.statusTags` (most 'preferred'; one 'probation' for variety), `payTerms`, `hireDate`, `managerId`. - [ ] **Step 3:** Populate `profile.achievements` for the field agents from their existing `MOCK_USERS` fields — `{ level: , xp, streak: streakDays, badges: achievements, tiers: [] }`. (Use the GamificationContext level-title thresholds for a level number, or store xp and let the UI derive.) - [ ] **Step 4 — store API:** add to the context value: - `resolveProfile(personId)` — returns the person's `profile` (merged onto their base identity from `resolvePerson`), or a sensible empty default. - `updateOwnProfile(personId, selfPatch)` — shallow-merges into `profile.self` (used by the self-service page). - `setSkillScore(personId, skillId, score)` — upserts a `company.skillScores` entry. - `addCatalogSkill(skill)` / `updateCatalogSkill(id, patch)` — manage `SKILL_CATALOG` (owner). Follow the existing `updateProject`-style `setX(prev => …)` pattern; store people in a state array if not already (profiles live on the existing people arrays — pick ONE canonical place to store the editable profile, e.g. a `profiles` state map keyed by personId, seeded from the people, so edits don't require mutating four different arrays). Expose `profiles` + the mutators. - [ ] **Step 5 (verify):** `npm run build` → `✓ built`, 0 duplicate-key warnings. `git grep -c "skillScores" -- src/data/mockStore.jsx` ≥ 12; `SKILL_CATALOG` exported; `resolveProfile`/`updateOwnProfile`/`setSkillScore` on the context value. - [ ] **Step 6:** commit `git commit -am "feat(data): add org skill catalog + two-layer person profiles + profile store API"`. ## Task 2: Remap dispatch reps + ground recommendations in profile facts **Files:** `src/data/mockStore.jsx` only. **After T1.** - [ ] **Step 1 — remap `DISPATCH_REPS`** to canonical people. For each REP-##, set `personId` to a field agent (REP-01→e1 Cody Tatum, REP-02→e2 Hannah Reyes, REP-03→e3 Travis Boone, REP-04→e4 Shelby Greer, REP-05→e5 Dalton Pruitt) and set `name`/`initials` to that person; replace the `skills: [...]` string array with the catalog skill ids the person scores highest on (from their `profile.company.skillScores`). Keep lat/lng/capacity/rating/closeRate. (Keep `id: 'REP-##'` if changing it is risky, but add `personId`; OR switch `id` to the person id — your call, just make recommendations resolve.) - [ ] **Step 2 — fix other fake ids:** `DISPATCH_LEADS_INITIAL.canvassedBy` (`U-05 Marcus Webb`, `U-07 Priya Nair`) → canonical field agents (e.g. e1 Cody Tatum, e2 Hannah Reyes). `DISPATCH_WEEKLY_STATS.repBreakdown` keys → re-key to the canonical person/rep ids used in Step 1. - [ ] **Step 3 — ground `DISPATCH_RECOMMENDATIONS`:** rewrite each recommendation's `reasons[]`, `stormReasons[]`, `aiInsight`, and `factors.skillMatch` so they **cite the cited rep's real profile facts**: - `factors.skillMatch` = the rep's actual `skillScore` for the skill the lead needs (e.g. an insurance-claim lead → the rep's `sk_insurance_claim` score). - `reasons[]` include a fact-citing chip, e.g. `"Insurance Claim 92/100 — top-rated for carrier work"`, `"Carries a tarp + drone on the truck"` (from `profile.self.equipment`). - `aiInsight` references a concrete fact: e.g. `"Cody scores 92 on Emergency Tarp and carries a tarp kit — best fit for a storm-window job."` Each recommendation must be traceable: the number/equipment it cites must EXIST in the cited rep's profile (this is the acceptance test). - [ ] **Step 4 (verify):** `npm run build` → `✓ built`. `git grep -n "Nina Patel\|Derek Walsh\|Carlos Mendez\|Marcus Webb\|Priya Nair" -- src/data/mockStore.jsx` → **none** (fake names gone). Spot-check 2 recommendations: the skill score / equipment cited matches the rep's profile. - [ ] **Step 5:** commit `git commit -am "feat(dispatch): map dispatch reps to canonical people; ground recommendations in real profile facts"`. ## Task 3: Self-service User Details page **Files:** Create `src/pages/UserDetailsPage.jsx`; edit `src/App.jsx`. **Phase B (parallel).** - [ ] **Step 1:** Build `UserDetailsPage.jsx` — the logged-in person (`useAuth().user`) edits THEIR OWN `profile.self`. Pull `resolveProfile(user.id)` + `updateOwnProfile` from the store. Sections (editable): **Identity** (preferredName, nickname, pronouns, gender, sex — all optional, clearly labelled optional; photo as a URL/objectURL upload), **Contact** (phone, altContact, address, emergencyContact), **About** (bio, yearsExperience, languages, specialties, equipment — equipment as add/remove chips), **Availability** (availability, serviceAreas). For subcontractors also show **Credentials** (license, insurance COI + expiry, w9Status). On save, call `updateOwnProfile(user.id, patch)`. Show the **read-only** `achievements` (level/xp/streak/badges) if present. Do NOT show the `company` layer (that's owner-only). Match app dark styling. - [ ] **Step 2 (routes):** import the page; add a universal route `/profile` → `UserDetailsPage`, gated to all internal roles `['OWNER','ADMIN','FIELD_AGENT','CONTRACTOR','SUBCONTRACTOR']` (customers keep `/portal/profile`). Add a nav/menu entry if there's an obvious place (e.g. the user dropdown) — optional. - [ ] **Step 3 (verify):** `npm run build` → `✓ built`. Dev-run as a field agent (login a field agent) → `/profile` shows their profile; editing nickname/equipment and saving persists in session. - [ ] **Step 4:** commit `git commit -am "feat(profile): self-service User Details page for all internal roles"`. ## Task 4: Owner Person Details view **Files:** `src/pages/owner/PeopleDirectory.jsx` only. **Phase B (parallel).** - [ ] **Step 1:** Expand the directory's people source to include the **field agents (e1–e5)** and **subcontractors (sub_001–005)** alongside `personnel` — pull from the store (the people arrays / `users`) so the owner sees the whole roster, grouped or filterable by role. - [ ] **Step 2:** Build/extend the detail panel to a full **Person Details** view showing BOTH layers (owner is authorised): the `self` profile (bio, contact, equipment, specialties, availability, achievements badges) AND the `company` layer (internalNotes, statusTags, payTerms, hireDate, manager) AND a **Skill Scores** table — each `SKILL_CATALOG` skill with the person's score (0–100 bar or 'N/A'). - [ ] **Step 3 (owner editing):** let the owner **set skill scores** (a number input / slider per catalog skill → `setSkillScore(personId, skillId, value)`) and edit `internalNotes`/`statusTags`. Wire the previously-stub "Edit"/notes affordances. (Skill catalog DEFINITION lives in Org Settings — T5; here the owner just scores people against the catalog.) - [ ] **Step 4 (verify):** `npm run build` → `✓ built`. Dev-run as owner → People shows e1–e5 + subs; opening a person shows self+company+skill scores+achievements; setting a score persists and is reflected (and would feed dispatch). - [ ] **Step 5:** commit `git commit -am "feat(owner): Person Details view — full roster, two-layer profile, skill scoring"`. ## Task 5: Skill Catalog editor + canonical leaderboard **Files:** `src/pages/owner/OrgSettings.jsx`, `src/context/GamificationContext.jsx`. **Phase B (parallel).** - [ ] **Step 1 (OrgSettings):** add a **Skill Catalog** section where the owner manages `skillCatalog` — list each skill (name + description), add a new skill (`addCatalogSkill({ id, name, description })`), and edit existing (`updateCatalogSkill`). Match the OrgSettings styling/section pattern. - [ ] **Step 2 (GamificationContext):** replace the fake `LEADERBOARD_MOCK` names (Alex M., Jessica R., …) with the canonical field agents (Cody Tatum, Hannah Reyes, Travis Boone, Shelby Greer, Dalton Pruitt) and plausible XP values consistent with their `MOCK_USERS.xp`. Keep the logged-in-user substitution behavior. - [ ] **Step 3 (verify):** `npm run build` → `✓ built`. Owner can add a skill in Org Settings (appears in the catalog + as a scorable row on Person Details). Leaderboard shows canonical names. - [ ] **Step 4:** commit `git commit -am "feat(skills): org skill-catalog editor; canonicalize ProCanvas leaderboard names"`. ## Task 6: Verify (read-only) - [ ] `npm run build` clean, 0 duplicate-key warnings. - [ ] **No fake people left in dispatch/gamification:** `git grep -nE "Nina Patel|Derek Walsh|Carlos Mendez|Aisha Kumar|Tom Bradley|Marcus Webb|Priya Nair|Alex M\.|Jessica R\." -- src` → none. - [ ] **Dispatch grounded (the headline acceptance):** pick 2 `DISPATCH_RECOMMENDATIONS`; the `skillMatch` value and any skill/equipment named in `reasons`/`aiInsight` EXIST in the cited rep's `profile.company.skillScores` / `profile.self.equipment`. Dev-run: open LynkDispatch → a recommendation card shows a canonical rep + a rationale that quotes a real number ("Emergency Tarp 92/100 · carries a tarp"). - [ ] **Profiles:** ≥ 12 people have `profile` with skillScores; `SKILL_CATALOG` exported + editable in Org Settings. - [ ] **Self page:** a field agent can open `/profile`, edit their self layer (nickname/equipment), save, and see it persist; the `company` layer is NOT shown to them. - [ ] **Owner view:** owner Person Details shows self+company+skill scores+achievements for e1–e5 and subs; setting a skill score persists. --- ## Self-review checklist - [ ] `SKILL_CATALOG` + per-person two-layer `profile` (self/company/achievements) added; store API (`resolveProfile`/`updateOwnProfile`/`setSkillScore`/`addCatalogSkill`) exposed. - [ ] Dispatch reps mapped to canonical field agents; recommendations cite real skill scores + equipment; all fake names gone (dispatch, canvassedBy, weekly stats, leaderboard). - [ ] Self-service User Details page (all internal roles) edits the self layer only; achievements read-only; company layer hidden. - [ ] Owner Person Details shows the full roster + both layers + skill scoring; Org Settings edits the catalog. - [ ] Build clean; a dispatch rationale demonstrably quotes a value that exists in the cited rep's profile. ## Notes for Plan 8 Plan 8 (ABAC) replaces the role checks that gate the `company` profile layer + skill scoring with policies (`can(subject, 'view', profile, {field:'company'})`), and the coarse route guards with the policy engine. The `resolveProfile` self/company split here is exactly the field-level visibility ABAC will formalise. Org Settings (skill catalog editor here) becomes the policy/attribute editor too.