From 159ed3eb862f1c4408fbdbdb51abcfeeeb3be6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 14 May 2026 17:44:36 +0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20PDD=20regions=20field=20=E2=80=94?= =?UTF-8?q?=201=20TDD=20task=20+=20verify=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation plan для regions multi-select autocomplete в PDD (spec: 4f60add docs/superpowers/specs/2026-05-14-pdd-regions-field-design.md). Task 1 (atomic TDD): - Step 1: read current state - Step 2: append 3 failing tests (chips render / select-encodes / clear-resets) - Step 3: verify 3 RED - Step 4: implement (REGIONS import + selectedRegions ref + maskToCodes helper + watch + reseed line + template autocomplete) - Step 5: 17 PDD tests pass - Step 6: vue-tsc + ESLint 0 errors - Step 7: ProjectsView integration tests still 8/1sk - Step 8: atomic commit Task 2 (verify-only): - Full vitest suite 92f/758+3sk - Vite build sanity - Visual smoke 8-step handoff to user Spec coverage: 100% (verified inline in plan §Self-Review). Out-of-plan: composable extraction / NewProjectDialog backport TODO / bigint / mobile — all explicitly deferred. NB env quirk: Write/Edit may silently fail on cyrillic-path — workaround через ASCII-Temp + PowerShell Copy-Item задокументирован в plan header. --- .../plans/2026-05-14-pdd-regions-field.md | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-14-pdd-regions-field.md diff --git a/docs/superpowers/plans/2026-05-14-pdd-regions-field.md b/docs/superpowers/plans/2026-05-14-pdd-regions-field.md new file mode 100644 index 00000000..f9c9e211 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-pdd-regions-field.md @@ -0,0 +1,282 @@ +# PDD Regions Field Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Добавить редактирование регионов проекта в `ProjectDetailsDrawer` через `v-autocomplete` с bidirectional bitmask binding (codes[] ↔ region_mask + region_mode). + +**Architecture:** In-place port рецепта из NewProjectDialog.vue (Подход A). `selectedRegions: Ref` reactive ↔ `form.region_mask` + `form.region_mode` через watch (forward) + `maskToCodes` helper (reverse при reseedFromProject). Backend без изменений (Task 5 onSave уже шлёт оба поля). Sentinel code:0 «Вся РФ» исключён из selectableRegions (fixes latent NewProjectDialog bug). + +**Tech Stack:** Vue 3.5 + Vuetify 3.12 v-autocomplete + Pinia + Vitest jsdom + axios. + +**Spec:** [../specs/2026-05-14-pdd-regions-field-design.md](../specs/2026-05-14-pdd-regions-field-design.md) commit `4f60add`. + +**Environment quirk:** Repo path `c:\моя\проекты\портал crm\Документация\` contains cyrillic — `Write`/`Edit` tools may silently fail intermittently (memory quirk #85). Workaround: Write→`C:\Users\Administrator\AppData\Local\Temp\`→PowerShell `Copy-Item`. Try Edit first; if file size unchanged or `git status` empty after operation — fall back to Temp+Copy. + +--- + +## File Structure + +| Path | Responsibility | Status | +|---|---|---| +| `app/resources/js/components/projects/ProjectDetailsDrawer.vue` | Add: REGIONS import / `selectedRegions` ref / `selectableRegions` filter / `maskToCodes` helper / watch / reseed line / template autocomplete block | **Modify** | +| `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` | Add: 3 new `it()` blocks at end of describe | **Modify** | + +Backend, store, NewProjectDialog — **no changes**. + +--- + +## Task 1: Regions field — failing tests + implementation + commit + +**Files:** + +- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue` +- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` + +- [ ] **Step 1: Read current component & test files** + +```bash +cd "c:/моя/проекты/портал crm/Документация" && cat app/resources/js/components/projects/ProjectDetailsDrawer.vue | head -80 +``` + +Note current state: 14 tests passing, imports `{ reactive, computed, onMounted, onBeforeUnmount, watch, ref }` from 'vue' + `axios` + `useProjectsStore` + type `Project`. Has `form`, `reseedFromProject`, watch on `props.project?.id`, `onSave`, `onPause`, `onDelete`. + +- [ ] **Step 2: Write 3 failing tests** + +Append to `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` after the existing last `it()` block (after Delete tests, inside the same `describe('ProjectDetailsDrawer', ...)`). + +NB: `axios.patch as ReturnType` cast pattern is established in this file from Task 5. + +```ts +it('renders region chips when project has non-zero region_mask', async () => { + const withRegions: Project = { ...sampleProject, region_mask: 6, region_mode: 'exclude' }; + const wrapper = mount(ProjectDetailsDrawer, { props: { project: withRegions } }); + await wrapper.vm.$nextTick(); + const text = wrapper.text(); + expect(text).toContain('Адыгея'); + expect(text).toContain('Башкортостан'); +}); + +it('selecting regions encodes mask + sets mode=exclude on save', async () => { + (axios.patch as ReturnType).mockResolvedValueOnce({ data: { data: sampleProject } }); + const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } }); + const autocomplete = wrapper.getComponent({ name: 'VAutocomplete' }); + await autocomplete.vm.$emit('update:model-value', [3, 5]); + await wrapper.vm.$nextTick(); + await wrapper.get('[data-testid="pdd-save"]').trigger('click'); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(axios.patch).toHaveBeenCalledWith( + '/api/projects/42', + expect.objectContaining({ region_mask: 40, region_mode: 'exclude' }), + ); +}); + +it('clearing all regions resets mask=0 + mode=include on save', async () => { + (axios.patch as ReturnType).mockResolvedValueOnce({ data: { data: sampleProject } }); + const withRegions: Project = { ...sampleProject, region_mask: 6, region_mode: 'exclude' }; + const wrapper = mount(ProjectDetailsDrawer, { props: { project: withRegions } }); + const autocomplete = wrapper.getComponent({ name: 'VAutocomplete' }); + await autocomplete.vm.$emit('update:model-value', []); + await wrapper.vm.$nextTick(); + await wrapper.get('[data-testid="pdd-save"]').trigger('click'); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(axios.patch).toHaveBeenCalledWith( + '/api/projects/42', + expect.objectContaining({ region_mask: 0, region_mode: 'include' }), + ); +}); +``` + +- [ ] **Step 3: Run, verify 3 fail** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "region" +``` + +Expected: 3 failed (no autocomplete in template + no chips rendered + no region_mask in payload). + +- [ ] **Step 4: Implement in component** + +Edit [app/resources/js/components/projects/ProjectDetailsDrawer.vue](../../../app/resources/js/components/projects/ProjectDetailsDrawer.vue): + +**4a.** Add import after the existing `import { useProjectsStore } from '../../stores/projectsStore';`: + +```ts +import { REGIONS } from '../../constants/regions'; +``` + +**4b.** Add right after `const dayLabels = ...` line (or near other reactive const declarations, before lifecycle hooks): + +```ts +const selectedRegions = ref([]); +const selectableRegions = REGIONS.filter((r) => r.code !== 0); + +function maskToCodes(mask: number): number[] { + const codes: number[] = []; + for (let i = 1; i <= 31; i++) if (mask & (1 << i)) codes.push(i); + return codes; +} + +watch(selectedRegions, (codes) => { + if (codes.length === 0) { + form.region_mask = 0; + form.region_mode = 'include'; + } else { + form.region_mask = codes.reduce((acc, c) => (c >= 1 && c <= 31 ? acc | (1 << c) : acc), 0); + form.region_mode = 'exclude'; + } +}); +``` + +**4c.** Inside `reseedFromProject(p)`, AFTER the existing `form.region_mask = p.region_mask ?? 0;` line, add: + +```ts +selectedRegions.value = maskToCodes(form.region_mask); +``` + +**4d.** In template, find the existing «Лимит лидов в день» ``** (and BEFORE the `
` with «Дни недели приёма»): + +```vue +
+ Регионы (пусто = вся РФ) + +
+``` + +NB: `` auto-imported via vite-plugin-vuetify. + +- [ ] **Step 5: Run all PDD tests, verify 17 pass** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts +``` + +Expected: 17 passed (14 pre-existing + 3 new). + +- [ ] **Step 6: Run vue-tsc + ESLint regression check** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npx eslint resources/js/components/projects/ProjectDetailsDrawer.vue tests/Frontend/ProjectDetailsDrawer.spec.ts && npx vue-tsc --noEmit 2>&1 | head -10 +``` + +Expected: ESLint 0 errors / 0 warnings. vue-tsc 0 errors. + +- [ ] **Step 7: Run ProjectsView integration tests (regression)** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npx vitest run tests/Frontend/ProjectsView.spec.ts +``` + +Expected: 8 passed + 1 skipped (no regression from drawer changes). + +- [ ] **Step 8: Commit atomically** + +```bash +cd "c:/моя/проекты/портал crm/Документация" && git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts +``` + +```bash +git commit -m "feat(pdd): regions multi-select autocomplete + bitmask binding" +``` + +Pre-commit hook (lefthook) will run gitleaks + markdownlint + cspell + stylelint + eslint-vue automatically. **DO NOT use `--no-verify`.** If a hook fails, fix root cause before retrying. + +--- + +## Task 2: Full regression sweep + visual smoke handoff + +**No new code.** Verification only. + +- [ ] **Step 1: Full Vitest suite** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npm run test:vue 2>&1 | tail -8 +``` + +Expected: 92 files passed, 758 tests passed + 3 skipped (delta vs baseline of 755+3sk after Task 1 +3 tests = 758). + +- [ ] **Step 2: Vite production build (sanity)** + +```bash +cd "c:/моя/проекты/портал crm/Документация/app" && npm run build 2>&1 | tail -5 +``` + +Expected: `✓ built in ` zero errors. ProjectsView chunk size may grow by ~1 kB (v-autocomplete + REGIONS-import) — acceptable. + +- [ ] **Step 3: Document visual-smoke handoff to user** + +Per spec §6 visual smoke (8 steps): + +1. Open /projects → select project «Доставка еды (СМС)» (or any) → drawer opens. +2. See «Регионы (пусто = вся РФ)» field between Лимит and Дни. +3. Click input → 31-region dropdown (NO «Вся РФ»). +4. Select Москва + Санкт-Петербург → 2 chips appear. +5. Click Сохранить → toast, Network tab shows PATCH with `region_mask` non-zero + `region_mode=exclude`. +6. Close drawer → reopen same project → 2 chips re-rendered (reverse-decompose works). +7. Click ✕ on a chip → chip removed. +8. Click clearable X → all chips removed → save → `region_mask=0`, `region_mode=include`. + +Report results to user. + +--- + +## Out of plan + +- Composable `useRegionsBitmask` extraction — Plan 6 when 3+ consumers. +- NewProjectDialog reverse-decompose backport (TODO line 172) — separate sweep. +- bigint mask для region codes >31 — schema change + Plan 6. +- Mobile-adaptive autocomplete (touch keyboard / full-screen modal) — Plan 6. + +--- + +## Self-Review + +**Spec coverage:** + +| Spec section | Task implementing it | +|---|---| +| §1 behavior table — 4 states | Task 1 Step 4 (watch + reseed) | +| §1 edge case `mode=include+mask≠0` legacy | Task 1 Step 4 (reseed decompose ignores mode) | +| §2 architecture Approach A | Task 1 Step 4 (all parts) | +| §3 script additions (selectedRegions / selectableRegions / maskToCodes / watch / reseed) | Task 1 Step 4a–4c | +| §3 template autocomplete block | Task 1 Step 4d | +| §4 data flow (reseed → watch → save) | Existing onSave (Task 5 PDD) + Task 1 Step 4 (other branches) | +| §5 error handling (?? 0, bit 0 skip, defensive guards) | Task 1 Step 4b (`(c >= 1 && c <= 31 ?` guard + loop `for i=1..31`) | +| §6 three Vitest tests | Task 1 Steps 2–5 | +| §6 visual smoke 8 steps | Task 2 Step 3 (handoff to user) | +| §6 regressions (14 PDD + 5 integration + vue-tsc + ESLint) | Task 1 Steps 5–7 + Task 2 Step 1–2 | + +All spec sections mapped. No gaps. + +**Placeholder scan:** No "TBD" / "TODO" / vague. All code in 4a–4d is concrete. All `expect.objectContaining` assertions use literal values (mask 40 = bits 3+5 = 8+32; mask 6 = bits 1+2 = 2+4). + +**Type consistency:** + +- `selectedRegions: Ref` declared in 4b, used unchanged in 4c reseed + watch + template. +- `selectableRegions` const, computed once at setup, no later re-binding. +- `maskToCodes(mask: number): number[]` signature stable. +- `form.region_mask` / `form.region_mode` already typed in existing `FormState` interface (Task 2 PDD). + +No issues found. + +--- + +## Files referenced (resolution) + +- Component: `app/resources/js/components/projects/ProjectDetailsDrawer.vue` (currently 14 tests passing, post-Task 7 PDD + Task 8 wire + polish-debt spec). +- Test: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` (14 cases, last `it` block is Delete confirm=false test). +- Constants: `app/resources/js/constants/regions.ts` (REGIONS array, 32 entries, code:0 sentinel + codes 1-31). +- Parent spec: `docs/superpowers/specs/2026-05-14-project-details-drawer-design.md` (commit `0d7f505` polish-debt). +- This plan's spec: `docs/superpowers/specs/2026-05-14-pdd-regions-field-design.md` (commit `4f60add`).