docs(plan): PDD regions field — 1 TDD task + verify sweep
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.
This commit is contained in:
@@ -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<number[]>` 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\<f>`→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<typeof vi.fn>` 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<typeof vi.fn>).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<typeof vi.fn>).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<number[]>([]);
|
||||
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 «Лимит лидов в день» `<label class="pdd-field">` block. **Insert immediately AFTER its closing `</label>`** (and BEFORE the `<div class="pdd-field">` with «Дни недели приёма»):
|
||||
|
||||
```vue
|
||||
<div class="pdd-field">
|
||||
<span class="pdd-label">Регионы (пусто = вся РФ)</span>
|
||||
<v-autocomplete
|
||||
v-model="selectedRegions"
|
||||
:items="selectableRegions"
|
||||
item-title="name"
|
||||
item-value="code"
|
||||
multiple
|
||||
chips
|
||||
clearable
|
||||
density="comfortable"
|
||||
hide-details
|
||||
data-testid="pdd-regions"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
NB: `<v-autocomplete>` 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 <Ns>` 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<number[]>` 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`).
|
||||
Reference in New Issue
Block a user