feat(pdd): render project name/limit/days form fields

This commit is contained in:
Дмитрий
2026-05-14 14:21:07 +03:00
parent 7d77187eb3
commit fa01951d27
2 changed files with 140 additions and 2 deletions
@@ -1,14 +1,104 @@
<script setup lang="ts">
import { reactive, computed } from 'vue';
import type { Project } from '../../stores/projectsStore';
defineProps<{ project: Project | null }>();
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">{{ project.name }}</header>
<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>
@@ -26,4 +116,20 @@ defineEmits<{ close: []; saved: [] }>();
z-index: 5;
}
.project-details-drawer.open { transform: translateX(0); }
.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; }
</style>
@@ -2,6 +2,22 @@ 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';
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',
};
describe('ProjectDetailsDrawer', () => {
beforeEach(() => setActivePinia(createPinia()));
@@ -12,4 +28,20 @@ describe('ProjectDetailsDrawer', () => {
expect(aside.exists()).toBe(true);
expect(aside.classes()).not.toContain('open');
});
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);
});
});