# ProjectDetailsDrawer 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:** Add right-side push-mode drawer with single-project edit form to `/projects`; route BulkActionsBar to ≥2 selected only.

**Architecture:** New custom `<aside>` component `ProjectDetailsDrawer.vue` rendered inside ProjectsView. CSS `transform: translateX(...)` + symmetric `padding-right` on `.projects-view` root drive the push. Backend untouched — existing `PATCH /api/projects/{id}` + `PATCH /toggle-active` + `DELETE /` endpoints used through existing Pinia `useProjectsStore`. Spec: [../specs/2026-05-14-project-details-drawer-design.md](../specs/2026-05-14-project-details-drawer-design.md).

**Tech Stack:** Vue 3.5 + Vuetify 3.12 + Pinia 3.0 + TypeScript + Vitest 4.1 (jsdom) + axios. Forest palette tokens (`--liderra-teal #0F6E56`, `--liderra-line #e6e2d6`).

**Environment quirk:** Write/Edit tools currently silently fail on this repo's cyrillic path (`c:\моя\проекты\портал crm\...`). Workaround: write new files to `C:\Users\Administrator\AppData\Local\Temp\<name>` then `Copy-Item` via PowerShell into the final cyrillic destination. Edits to existing files use the same pattern: read file content into Temp, modify, copy back. Verify via `Test-Path` after every copy. See quirk #85 in `memory/feedback_environment.md`.

---

## File Structure

| Path | Responsibility | Status |
|---|---|---|
| `app/resources/js/components/projects/ProjectDetailsDrawer.vue` | Component: header, form, footer with 4 actions. Owns local form state, axios PATCH calls. | **Create** |
| `app/tests/Frontend/ProjectDetailsDrawer.spec.ts` | Unit tests in jsdom (11 cases per spec §6). | **Create** |
| `app/resources/js/views/ProjectsView.vue` | Hosts drawer + condition swap. Adds `singleSelectedProject` computed + `.has-drawer` class. | **Modify** (`:78` + script + style) |
| `app/tests/Frontend/ProjectsView.spec.ts` | Integration tests for 0/1/≥2 selected states + close interaction. | **Modify** (5 new cases) |

Backend, `projectsStore.ts`, `BulkActionsBar.vue`, `ProjectCard.vue`, `NewProjectDialog.vue` — **no changes**.

---

## Task 1: Scaffold component + failing test "renders nothing when project=null"

**Files:**

- Create: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Create: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write failing test for null-project no-render**

`app/tests/Frontend/ProjectDetailsDrawer.spec.ts`:

```ts
import { describe, it, expect, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import ProjectDetailsDrawer from '../../resources/js/components/projects/ProjectDetailsDrawer.vue';

describe('ProjectDetailsDrawer', () => {
    beforeEach(() => setActivePinia(createPinia()));

    it('does not have .open class when project=null', () => {
        const wrapper = mount(ProjectDetailsDrawer, { props: { project: null } });
        const aside = wrapper.find('aside.project-details-drawer');
        expect(aside.exists()).toBe(true);
        expect(aside.classes()).not.toContain('open');
    });
});
```

- [ ] **Step 2: Run test, verify it fails because component does not exist**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: FAIL with "Failed to resolve import './ProjectDetailsDrawer.vue'" or "Cannot find module".

- [ ] **Step 3: Create minimal component to pass the test**

`app/resources/js/components/projects/ProjectDetailsDrawer.vue`:

```vue
<script setup lang="ts">
import type { Project } from '../../stores/projectsStore';

const props = defineProps<{ project: Project | null }>();
defineEmits<{ close: []; saved: [] }>();
</script>

<template>
    <aside class="project-details-drawer" :class="{ open: project !== null }">
        <div v-if="project" class="pdd-content">
            <header class="pdd-head">{{ project.name }}</header>
        </div>
    </aside>
</template>

<style scoped>
.project-details-drawer {
    position: fixed; top: 0; right: 0; bottom: 0;
    width: 480px;
    background: var(--liderra-surface, #ffffff);
    border-left: 1px solid var(--liderra-line, #e6e2d6);
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.06);
    transform: translateX(100%);
    transition: transform 240ms cubic-bezier(0.16, 1, 0.3, 1);
    display: flex; flex-direction: column;
    z-index: 5;
}
.project-details-drawer.open { transform: translateX(0); }
</style>
```

- [ ] **Step 4: Run test, verify it passes**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 1 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "test(pdd): scaffold ProjectDetailsDrawer + null-project no-open test"
```

---

## Task 2: Render project fields (name / daily_limit_target / delivery_days)

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write failing test for project fields render**

Append in `tests/Frontend/ProjectDetailsDrawer.spec.ts` after the existing test:

```ts
import type { Project } from '../../resources/js/stores/projectsStore';

const sampleProject: Project = {
    id: 42,
    name: 'Натяжные потолки',
    signal_type: 'call',
    signal_identifier: '79161112233',
    daily_limit_target: 30,
    delivered_today: 0,
    is_active: true,
    archived_at: null,
    region_mask: 0,
    region_mode: 'include',
    delivery_days_mask: 31, // Mon-Fri
    sync_status: 'pending',
};

it('renders project name + daily_limit_target + delivery_days', () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const aside = wrapper.find('aside.project-details-drawer');
    expect(aside.classes()).toContain('open');
    expect(wrapper.text()).toContain('Натяжные потолки');
    const nameInput = wrapper.get('input[data-testid="pdd-name"]');
    expect((nameInput.element as HTMLInputElement).value).toBe('Натяжные потолки');
    const limitInput = wrapper.get('input[data-testid="pdd-limit"]');
    expect((limitInput.element as HTMLInputElement).value).toBe('30');
    // Days mask 31 = bits 0..4 = Mon..Fri (5 days active)
    const dayBtns = wrapper.findAll('button[data-testid^="pdd-day-"]');
    expect(dayBtns.length).toBe(7);
    const activeBtns = dayBtns.filter(b => b.classes().includes('active'));
    expect(activeBtns.length).toBe(5);
});
```

- [ ] **Step 2: Run test, verify it fails**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "renders project name"
```

Expected: FAIL with "Cannot find data-testid='pdd-name'" or similar.

- [ ] **Step 3: Expand component with form + fields**

Replace `<script setup>` and `<template>` sections (keep `<style>` from Task 1):

```vue
<script setup lang="ts">
import { reactive, ref, watch, computed } from 'vue';
import type { Project } from '../../stores/projectsStore';

const props = defineProps<{ project: Project | null }>();
defineEmits<{ close: []; saved: [] }>();

interface FormState {
    name: string;
    daily_limit_target: number;
    region_mask: number;
    region_mode: 'include' | 'exclude';
    delivery_days_mask: number;
    sms_senders: string[];
    sms_keyword: string;
}

const form = reactive<FormState>({
    name: '',
    daily_limit_target: 50,
    region_mask: 0,
    region_mode: 'include',
    delivery_days_mask: 127,
    sms_senders: [],
    sms_keyword: '',
});

function reseedFromProject(p: Project | null): void {
    if (!p) return;
    form.name = p.name;
    form.daily_limit_target = p.daily_limit_target;
    form.region_mask = p.region_mask ?? 0;
    form.region_mode = (p.region_mode ?? 'include') as 'include' | 'exclude';
    form.delivery_days_mask = p.delivery_days_mask ?? 127;
    form.sms_senders = p.sms_senders ?? [];
    form.sms_keyword = p.sms_keyword ?? '';
}
reseedFromProject(props.project);

const activeDays = computed<boolean[]>(() => {
    const mask = form.delivery_days_mask;
    return Array.from({ length: 7 }, (_, i) => (mask & (1 << i)) !== 0);
});

function toggleDay(i: number): void {
    form.delivery_days_mask ^= (1 << i);
}

const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
</script>

<template>
    <aside class="project-details-drawer" :class="{ open: project !== null }">
        <div v-if="project" class="pdd-content">
            <header class="pdd-head">
                <div class="pdd-title">{{ project.name }}</div>
                <button class="pdd-close" data-testid="pdd-close" @click="$emit('close')">✕</button>
            </header>

            <div class="pdd-body">
                <label class="pdd-field">
                    <span class="pdd-label">Название</span>
                    <input v-model="form.name" data-testid="pdd-name" class="pdd-input" />
                </label>

                <label class="pdd-field">
                    <span class="pdd-label">Лимит лидов в день</span>
                    <input
                        v-model.number="form.daily_limit_target"
                        type="number"
                        min="1"
                        max="10000"
                        data-testid="pdd-limit"
                        class="pdd-input"
                    />
                </label>

                <div class="pdd-field">
                    <span class="pdd-label">Дни недели приёма</span>
                    <div class="pdd-days">
                        <button
                            v-for="(label, i) in dayLabels"
                            :key="i"
                            type="button"
                            :data-testid="`pdd-day-${i}`"
                            :class="['pdd-day', { active: activeDays[i] }]"
                            @click="toggleDay(i)"
                        >
                            {{ label }}
                        </button>
                    </div>
                </div>
            </div>

            <footer class="pdd-foot">
                <div class="pdd-foot-left"></div>
                <div class="pdd-foot-right">
                    <button class="pdd-btn pdd-btn-text" data-testid="pdd-cancel" @click="$emit('close')">Отмена</button>
                    <button class="pdd-btn pdd-btn-primary" data-testid="pdd-save">Сохранить</button>
                </div>
            </footer>
        </div>
    </aside>
</template>
```

Append to `<style scoped>`:

```css
.pdd-content { display: flex; flex-direction: column; height: 100%; }
.pdd-head { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--liderra-line, #e6e2d6); }
.pdd-title { font-weight: 600; font-size: 16px; }
.pdd-close { background: none; border: 0; cursor: pointer; font-size: 18px; padding: 4px; }
.pdd-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; flex: 1; overflow-y: auto; }
.pdd-field { display: flex; flex-direction: column; gap: 4px; }
.pdd-label { font-size: 12px; color: #6b6f72; }
.pdd-input { padding: 8px 10px; border: 1px solid var(--liderra-line, #e6e2d6); border-radius: 6px; font: inherit; }
.pdd-days { display: flex; gap: 4px; }
.pdd-day { padding: 6px 10px; border: 1px solid var(--liderra-line, #e6e2d6); background: #ffffff; border-radius: 4px; cursor: pointer; font: inherit; }
.pdd-day.active { background: #0f6e56; color: #ffffff; border-color: #0f6e56; }
.pdd-foot { display: flex; justify-content: space-between; padding: 12px 20px; border-top: 1px solid var(--liderra-line, #e6e2d6); }
.pdd-foot-left, .pdd-foot-right { display: flex; gap: 8px; }
.pdd-btn { padding: 6px 14px; border: 0; border-radius: 6px; cursor: pointer; font: inherit; }
.pdd-btn-text { background: transparent; color: #081319; }
.pdd-btn-primary { background: #0f6e56; color: #ffffff; }
```

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 2 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): render project name/limit/days form fields"
```

---

## Task 3: Close emits (X click / Cancel click / ESC keydown)

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write 3 failing tests**

Append after the previous tests:

```ts
it('emits close on X button click', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    await wrapper.get('[data-testid="pdd-close"]').trigger('click');
    expect(wrapper.emitted('close')).toHaveLength(1);
});

it('emits close on Cancel button click', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    await wrapper.get('[data-testid="pdd-cancel"]').trigger('click');
    expect(wrapper.emitted('close')).toHaveLength(1);
});

it('emits close on ESC keydown when project is set', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject }, attachTo: document.body });
    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
    await wrapper.vm.$nextTick();
    expect(wrapper.emitted('close')).toHaveLength(1);
    wrapper.unmount();
});

it('does NOT emit close on ESC when project=null', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: null }, attachTo: document.body });
    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
    await wrapper.vm.$nextTick();
    expect(wrapper.emitted('close')).toBeUndefined();
    wrapper.unmount();
});
```

X-click and Cancel-click tests will PASS (already wired in Task 2). The two ESC tests will FAIL.

- [ ] **Step 2: Run, verify the 2 ESC tests fail**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "ESC"
```

Expected: 2 failed (no close emit on ESC).

- [ ] **Step 3: Add ESC handler in lifecycle**

Add to `<script setup>` (after `reseedFromProject`):

```ts
import { onMounted, onBeforeUnmount } from 'vue';

const emit = defineEmits<{ close: []; saved: [] }>();

function onKey(e: KeyboardEvent): void {
    if (e.key === 'Escape' && props.project) emit('close');
}
onMounted(() => document.addEventListener('keydown', onKey));
onBeforeUnmount(() => document.removeEventListener('keydown', onKey));
```

NB: replace the existing `defineEmits` line (the bare-call form from Task 2) with the assigned `const emit = ...` form — both forms are valid, but we need a callable identifier for `onKey`.

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 6 passed (2 from prior tasks + 4 from this task).

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): emit close on X/Cancel/ESC"
```

---

## Task 4: Form reseed on project.id change

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write failing test**

Append:

```ts
it('reseeds form when project.id changes', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const nameInputBefore = wrapper.get('input[data-testid="pdd-name"]').element as HTMLInputElement;
    expect(nameInputBefore.value).toBe('Натяжные потолки');

    const other: Project = { ...sampleProject, id: 99, name: 'Окна СПб', daily_limit_target: 50 };
    await wrapper.setProps({ project: other });

    const nameInputAfter = wrapper.get('input[data-testid="pdd-name"]').element as HTMLInputElement;
    expect(nameInputAfter.value).toBe('Окна СПб');
    expect((wrapper.get('input[data-testid="pdd-limit"]').element as HTMLInputElement).value).toBe('50');
});
```

- [ ] **Step 2: Run, verify fail**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "reseeds form"
```

Expected: FAIL — name input still shows "Натяжные потолки" (form not reseeded).

- [ ] **Step 3: Add watch in `<script setup>`**

Insert after `reseedFromProject(props.project);`:

```ts
watch(() => props.project?.id, () => {
    reseedFromProject(props.project);
});
```

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 7 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): reseed form on project.id change"
```

---

## Task 5: Save action — PATCH /api/projects/{id} (200 + 422)

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write 2 failing tests (success + validation)**

Append (after Task 4 test):

```ts
import { vi } from 'vitest';
import axios from 'axios';

vi.mock('axios');

it('Save: PATCH /api/projects/{id} + emits saved on 200', async () => {
    (axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ data: { data: sampleProject } });
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });

    await wrapper.get('[data-testid="pdd-name"]').setValue('Новое имя');
    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({ name: 'Новое имя', daily_limit_target: 30 }),
    );
    expect(wrapper.emitted('saved')).toHaveLength(1);
});

it('Save: 422 → errors reactive proставляется, no saved emit', async () => {
    (axios.patch as unknown as ReturnType<typeof vi.fn>).mockRejectedValueOnce({
        response: { status: 422, data: { errors: { name: ['Имя обязательно'] } } },
    });
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });

    await wrapper.get('[data-testid="pdd-save"]').trigger('click');
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();

    expect(wrapper.emitted('saved')).toBeUndefined();
    expect(wrapper.text()).toContain('Имя обязательно');
});
```

- [ ] **Step 2: Run, verify fail**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "Save"
```

Expected: 2 failed.

- [ ] **Step 3: Implement Save**

In `<script setup>` add (after watch block):

```ts
import axios from 'axios';

const saving = ref(false);
const errors = reactive<Record<string, string[]>>({});

async function onSave(): Promise<void> {
    if (!props.project) return;
    saving.value = true;
    Object.keys(errors).forEach((k) => delete errors[k]);
    try {
        const payload: Record<string, unknown> = {
            name: form.name,
            daily_limit_target: form.daily_limit_target,
            region_mask: form.region_mask,
            region_mode: form.region_mode,
            delivery_days_mask: form.delivery_days_mask,
        };
        if (props.project.signal_type === 'sms') {
            payload.sms_senders = form.sms_senders;
            payload.sms_keyword = form.sms_keyword;
        }
        await axios.patch(`/api/projects/${props.project.id}`, payload);
        emit('saved');
    } catch (e: unknown) {
        const err = e as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } };
        if (err.response?.status === 422 && err.response.data?.errors) {
            Object.assign(errors, err.response.data.errors);
        }
    } finally {
        saving.value = false;
    }
}
```

Wire button in `<template>`:

```vue
<button
    class="pdd-btn pdd-btn-primary"
    data-testid="pdd-save"
    :disabled="saving"
    @click="onSave"
>Сохранить</button>
```

Add error-display under fields (after `<input ... data-testid="pdd-name" ... />`):

```vue
<div v-if="errors.name" class="pdd-error" data-testid="pdd-error-name">{{ errors.name[0] }}</div>
```

Same for limit:

```vue
<div v-if="errors.daily_limit_target" class="pdd-error">{{ errors.daily_limit_target[0] }}</div>
```

Append to `<style scoped>`:

```css
.pdd-error { color: #dc2626; font-size: 12px; margin-top: 4px; }
```

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 9 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): Save action — PATCH /api/projects/{id} + 422 errors"
```

---

## Task 6: Pause/Resume + label switch on is_active

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write 2 failing tests**

Append:

```ts
import { useProjectsStore } from '../../resources/js/stores/projectsStore';

it('Pause button calls store.toggleActive', async () => {
    (axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ data: { data: { ...sampleProject, is_active: false } } });
    (axios.get as unknown as ReturnType<typeof vi.fn> | undefined)?.mockResolvedValue?.({ data: { data: [], meta: { total: 0 } } });
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const store = useProjectsStore();
    const spy = vi.spyOn(store, 'toggleActive').mockResolvedValueOnce(undefined);

    await wrapper.get('[data-testid="pdd-pause"]').trigger('click');
    expect(spy).toHaveBeenCalledWith(sampleProject);
});

it('Pause button label is "Возобновить" when is_active=false', () => {
    const paused: Project = { ...sampleProject, is_active: false };
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: paused } });
    expect(wrapper.get('[data-testid="pdd-pause"]').text()).toContain('Возобновить');
});

it('Pause button label is "Приостановить" when is_active=true', () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    expect(wrapper.get('[data-testid="pdd-pause"]').text()).toContain('Приостановить');
});
```

- [ ] **Step 2: Run, verify fail**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "Pause"
```

Expected: 3 failed.

- [ ] **Step 3: Implement Pause**

Add to `<script setup>` (after Save logic):

```ts
import { useProjectsStore } from '../../stores/projectsStore';

const store = useProjectsStore();

async function onPause(): Promise<void> {
    if (!props.project) return;
    await store.toggleActive(props.project);
}
```

Wire button in footer left-group:

```vue
<div class="pdd-foot-left">
    <button class="pdd-btn pdd-btn-warning" data-testid="pdd-pause" @click="onPause">
        {{ project.is_active ? '⏸ Приостановить' : '▶ Возобновить' }}
    </button>
</div>
```

Append CSS:

```css
.pdd-btn-warning { background: #f59e0b; color: #ffffff; }
```

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 12 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): Pause/Resume button with toggleActive + dynamic label"
```

---

## Task 7: Delete with confirm

**Files:**

- Modify: `app/tests/Frontend/ProjectDetailsDrawer.spec.ts`
- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue`

- [ ] **Step 1: Write 2 failing tests**

Append:

```ts
it('Delete: confirm=true → archive + close emit', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const store = useProjectsStore();
    const spy = vi.spyOn(store, 'archive').mockResolvedValueOnce(undefined);
    vi.stubGlobal('confirm', () => true);

    await wrapper.get('[data-testid="pdd-delete"]').trigger('click');
    await wrapper.vm.$nextTick();
    expect(spy).toHaveBeenCalledWith(42);
    expect(wrapper.emitted('close')).toBeDefined();

    vi.unstubAllGlobals();
});

it('Delete: confirm=false → no archive, no close', async () => {
    const wrapper = mount(ProjectDetailsDrawer, { props: { project: sampleProject } });
    const store = useProjectsStore();
    const spy = vi.spyOn(store, 'archive').mockResolvedValueOnce(undefined);
    vi.stubGlobal('confirm', () => false);

    await wrapper.get('[data-testid="pdd-delete"]').trigger('click');
    expect(spy).not.toHaveBeenCalled();
    expect(wrapper.emitted('close')).toBeUndefined();

    vi.unstubAllGlobals();
});
```

- [ ] **Step 2: Run, verify fail**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts -t "Delete"
```

Expected: 2 failed.

- [ ] **Step 3: Implement Delete**

Add to `<script setup>`:

```ts
async function onDelete(): Promise<void> {
    if (!props.project) return;
    const ok = window.confirm('Архивировать проект? Действие необратимо в Plan 5 (восстановление потребует ручного запроса).');
    if (!ok) return;
    await store.archive(props.project.id);
    emit('close');
}
```

Wire button in footer left-group (after Pause):

```vue
<button class="pdd-btn pdd-btn-error" data-testid="pdd-delete" @click="onDelete">🗄 Удалить</button>
```

Append CSS:

```css
.pdd-btn-error { background: #dc2626; color: #ffffff; }
```

- [ ] **Step 4: Run tests**

```bash
cd app && npx vitest run tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: 14 passed.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/projects/ProjectDetailsDrawer.vue app/tests/Frontend/ProjectDetailsDrawer.spec.ts
git commit -m "feat(pdd): Delete button with confirm + archive + close"
```

---

## Task 8: ProjectsView wire (condition swap + drawer mount + computed + .has-drawer)

**Files:**

- Modify: `app/resources/js/views/ProjectsView.vue`

This task has no new tests — integration tests come in Task 9 against the modified ProjectsView.

- [ ] **Step 1: Modify `<script setup>` to add computed + handlers**

In `app/resources/js/views/ProjectsView.vue` after the existing imports (the `import { ref, onMounted, ... } from 'vue'` line), add `computed`:

```ts
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
```

Add new import for the drawer (after the existing `import ProjectCard ...` line):

```ts
import ProjectDetailsDrawer from '../components/projects/ProjectDetailsDrawer.vue';
```

After `const editing = ref<Project | null>(null);` add:

```ts
const singleSelectedProject = computed<Project | null>(() => {
    if (store.selectedIds.size !== 1) return null;
    const [id] = store.selectedIds;
    return store.items.find((p: Project) => p.id === id) ?? null;
});

function onDrawerClose(): void {
    store.clearSelection();
}
function onDrawerSaved(): void {
    void store.fetch();
}
```

- [ ] **Step 2: Modify `<template>` — swap condition + mount drawer + add class**

Find at `app/resources/js/views/ProjectsView.vue:78`:

```vue
<BulkActionsBar v-if="store.selectedIds.size > 0" />
```

Replace with:

```vue
<BulkActionsBar v-if="store.selectedIds.size >= 2" />

<ProjectDetailsDrawer
    :project="singleSelectedProject"
    @close="onDrawerClose"
    @saved="onDrawerSaved"
/>
```

Find `<div class="projects-view">` at line 2:

Replace with:

```vue
<div class="projects-view" :class="{ 'has-drawer': singleSelectedProject !== null }">
```

- [ ] **Step 3: Add `<style scoped>` push CSS**

Append to the existing `<style scoped>` block:

```css
.projects-view {
    transition: padding-right 240ms cubic-bezier(0.16, 1, 0.3, 1);
}
.projects-view.has-drawer {
    padding-right: 480px;
}
```

- [ ] **Step 4: Run pre-existing tests to ensure no regression**

```bash
cd app && npx vitest run tests/Frontend/ProjectsView.spec.ts tests/Frontend/ProjectsView.toolbar.spec.ts tests/Frontend/ProjectDetailsDrawer.spec.ts
```

Expected: all pre-existing pass + 14 new from PDD spec.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/views/ProjectsView.vue
git commit -m "feat(projects): integrate ProjectDetailsDrawer + swap bulk-bar condition >=2"
```

---

## Task 9: ProjectsView integration tests (5 cases for 0/1/≥2 states + close)

**Files:**

- Modify: `app/tests/Frontend/ProjectsView.spec.ts`

- [ ] **Step 1: Read existing spec to find an insertion point**

```bash
cd app && head -60 tests/Frontend/ProjectsView.spec.ts
```

Note the existing imports + test setup pattern; add new tests at the end of the file.

- [ ] **Step 2: Write 5 failing integration tests**

Append to `app/tests/Frontend/ProjectsView.spec.ts`:

```ts
import { describe as describe2, it as it2, expect as expect2 } from 'vitest';

describe2('ProjectsView × ProjectDetailsDrawer integration', () => {
    it2('0 selected → no drawer-open, no bulk-bar', async () => {
        const wrapper = await mountProjectsView({ items: [demoProject(1), demoProject(2)] });
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(false);
        expect2(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
        expect2(wrapper.find('.projects-view.has-drawer').exists()).toBe(false);
    });

    it2('1 selected → drawer open, bulk-bar hidden, .has-drawer class on view', async () => {
        const wrapper = await mountProjectsView({ items: [demoProject(1), demoProject(2)], selectedIds: [1] });
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(true);
        expect2(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
        expect2(wrapper.find('.projects-view.has-drawer').exists()).toBe(true);
    });

    it2('2 selected → drawer NOT open, bulk-bar visible, no .has-drawer', async () => {
        const wrapper = await mountProjectsView({ items: [demoProject(1), demoProject(2)], selectedIds: [1, 2] });
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(false);
        expect2(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(true);
        expect2(wrapper.find('.projects-view.has-drawer').exists()).toBe(false);
    });

    it2('Drawer @close → store.clearSelection → bulk-bar AND drawer hidden', async () => {
        const wrapper = await mountProjectsView({ items: [demoProject(1)], selectedIds: [1] });
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(true);
        await wrapper.findComponent({ name: 'ProjectDetailsDrawer' }).vm.$emit('close');
        await wrapper.vm.$nextTick();
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(false);
        expect2(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
    });

    it2('singleSelectedProject = null when selected id is not in items', async () => {
        const wrapper = await mountProjectsView({ items: [demoProject(1)], selectedIds: [999] });
        expect2(wrapper.find('aside.project-details-drawer.open').exists()).toBe(false);
    });
});

// Helpers — adapt to whatever the existing spec already exports / uses.
// If `mountProjectsView` and `demoProject` don't already exist in the file,
// extract them from the existing test setup at the top of the spec
// (look for `mount(ProjectsView, ...)` patterns).
function demoProject(id: number) {
    return {
        id,
        name: `Project ${id}`,
        signal_type: 'site' as const,
        signal_identifier: `domain-${id}.ru`,
        daily_limit_target: 50,
        delivered_today: 0,
        is_active: true,
        archived_at: null,
        region_mask: 0,
        region_mode: 'include',
        delivery_days_mask: 127,
        sync_status: 'ok' as const,
    };
}

async function mountProjectsView(opts: { items: ReturnType<typeof demoProject>[]; selectedIds?: number[] }) {
    // re-use the existing spec's mount setup. If your existing spec uses
    // a different mounting helper, replace the body of this function with that.
    const pinia = createTestingPinia({
        initialState: {
            projects: {
                items: opts.items,
                total: opts.items.length,
                filters: { signal_type: '', status: '', search: '', page: 1, per_page: 20 },
                selectedIds: new Set(opts.selectedIds ?? []),
                pendingIds: new Set(),
                loading: false,
                selectAllByFilter: false,
            },
        },
        stubActions: false,
    });
    const wrapper = mount(ProjectsView, { global: { plugins: [pinia, vuetify], stubs: { RouterLink: true } } });
    await wrapper.vm.$nextTick();
    return wrapper;
}
```

**NB:** If `createTestingPinia`, `vuetify`, or `mountProjectsView` already exist at the top of the spec, **delete** my local helper definitions and use the existing ones. Open the file and check first; do not duplicate.

- [ ] **Step 3: Run, verify all 5 fail OR pass depending on whether helpers are right**

```bash
cd app && npx vitest run tests/Frontend/ProjectsView.spec.ts
```

If failures are about missing helpers (`createTestingPinia` etc.), wire them per the existing spec's pattern. If failures are about behavior — that means Task 8 logic is incomplete; go back and fix.

- [ ] **Step 4: Iterate until all 5 pass**

```bash
cd app && npx vitest run tests/Frontend/ProjectsView.spec.ts
```

Expected: all pre-existing pass + 5 new pass.

- [ ] **Step 5: Commit**

```bash
git add app/tests/Frontend/ProjectsView.spec.ts
git commit -m "test(projects): integration tests for drawer × bulk-bar mutual exclusion"
```

---

## Task 10: Full regression run + visual smoke + final verify

**No new code. Verification only.**

- [ ] **Step 1: Run full Vitest suite**

```bash
cd app && npm run test:vue 2>&1 | tail -50
```

Expected: all tests pass. Baseline before this work: 91 files / 736 specs / 3 skipped / 0 failed (per memory). After: same + 14 (PDD) + 5 (integration) = 91 files / 755 specs / 3 skipped / 0 failed.

- [ ] **Step 2: Run vue-tsc type-check**

```bash
cd app && npm run type-check 2>&1 | tail -30
```

Expected: 0 errors.

- [ ] **Step 3: Run ESLint**

```bash
cd app && npm run lint:vue 2>&1 | tail -30
```

Expected: 0 errors.

- [ ] **Step 4: Visual smoke via browser**

Start dev-server (if not running):

```bash
cd app && npm run dev
```

Open `http://127.0.0.1:8000/projects` in a browser (Laravel artisan serve must also be running — `cd app && php artisan serve`).

Manual checks (per spec §6 Visual smoke):

1. Click checkbox on 1 project → drawer slides in from right within ~240ms, grid shifts left, bulk-bar hidden.
2. Click checkbox on 2nd project → drawer slides out, grid returns, bulk-bar appears at bottom.
3. Uncheck back to 1 → drawer slides in, bulk-bar disappears.
4. Press ESC → drawer slides out + checkbox unchecks.
5. Click X on drawer header → same.
6. Click "Отмена" → same.
7. Edit name → click "Сохранить" → drawer stays open, no errors, network tab shows `PATCH /api/projects/{id}` 200.
8. Click "⏸ Приостановить" → label flips to "▶ Возобновить", `PATCH /toggle-active` in network.
9. Click "🗄 Удалить" → confirm prompt appears. Cancel — no-op. Accept — `DELETE /api/projects/{id}` 204, drawer closes, project gone from grid.

If any step fails — return to the relevant Task and fix; commit the fix.

- [ ] **Step 5: Final summary**

After all 10 tasks completed, post a status:

```
Branch: main (or feature/...).
Commits: 9 (Tasks 1-9, Task 10 had no code).
Tests: <N> Vitest passed, 0 failed, vue-tsc 0, ESLint 0.
Visual smoke: 9/9 manual checks passed.
```

---

## Out of plan (Plan 6+)

- Confirm dialog when closing drawer with dirty form (spec §7).
- Optimistic update of grid after Save (spec §7).
- Mobile-adaptive layout (<768px viewport → fallback to existing modal-edit).
- Region multi-select autocomplete (region_mask binding via codes — present in NewProjectDialog, can be ported in a separate sweep). Until ported, region_mask stays whatever the backend has — no UI for changing it in this iteration. **NB:** if you want region editing in v1, add a Task between current 4 and 5 that ports the `selectedRegions[]` watch from `NewProjectDialog.vue:141-153` into `ProjectDetailsDrawer.vue` — Save in Task 5 already includes `region_mask` + `region_mode` in the payload.

---

## Self-Review

**Spec coverage (per spec §1-§6):**

| Spec section | Task(s) |
|---|---|
| §1 behavior — 0/1/≥2 states | Task 8 (wire) + Task 9 (integration tests) |
| §1 transitions — drawer slide / grid shift | Task 1 (CSS skeleton) + Task 8 (.has-drawer class) |
| §1 edge case — project disappears from items | Task 9 case "singleSelectedProject = null when selected id is not in items" |
| §2 architecture — custom aside + push | Task 1 (component CSS) + Task 8 (view CSS) |
| §2 endpoints — PATCH/toggle-active/DELETE | Task 5 (Save) + Task 6 (Pause) + Task 7 (Delete) |
| §3 props/emits | Task 1 (initial) + Task 3 (close emit) + Task 5 (saved emit) |
| §3 ESC handler | Task 3 |
| §3 form reseed on project change | Task 4 |
| §3 footer (4 buttons in 2 groups) | Task 2 (Cancel/Save) + Task 6 (Pause) + Task 7 (Delete) |
| §4 data flow | Implicitly covered by Tasks 5/6/7 + Task 8 integration |
| §5 error handling — 422 | Task 5 |
| §5 error handling — Form dirty + Cancel | No confirm by design (spec §5 MVP decision) — no task |
| §6 11 unit tests | Tasks 1-7 (14 actual tests, includes 3 extras for ESC null + Pause label) |
| §6 5 integration tests | Task 9 |
| §6 visual smoke | Task 10 |

All spec sections mapped to ≥1 task. **No gaps.**

**Placeholder scan:**

- No "TBD"/"TODO" in plan body.
- All code blocks contain complete content (not "add validation here" — explicit code).
- Task 9 has explicit fallback instruction ("if helpers exist, delete my local defs") instead of vague "adapt as needed".
- Task 10 has explicit 9 manual checks instead of "verify visually".

**Type consistency:**

- `Project` imported from `../../stores/projectsStore` consistently across component + test.
- `defineEmits<{ close: []; saved: [] }>` declared in Task 1, used unchanged through Task 7.
- `singleSelectedProject` computed signature in Task 8 matches spec §3.
- `onPause` / `onDelete` / `onSave` — function names stable across tasks.
- `data-testid` namespacing: `pdd-*` consistent (`pdd-name`, `pdd-limit`, `pdd-day-N`, `pdd-close`, `pdd-cancel`, `pdd-save`, `pdd-pause`, `pdd-delete`, `pdd-error-name`).

**Region editing:** Spec §3 calls out v-autocomplete for regions. Plan defers the autocomplete UI (Out of plan section) but Save in Task 5 already sends region_mask/region_mode through. This is intentional — region editing is non-trivial enough to warrant its own task and isn't a hard blocker for shipping the drawer. Documented in Out-of-plan with a precise integration recipe.

No issues found that require revision.
