Merge remote-tracking branch 'gitea/main' into fix/tg-zagolovok-obyavleniya

# Conflicts:
#	docs/observer/STATUS.md
This commit is contained in:
Дмитрий
2026-08-01 16:35:18 +03:00
16 changed files with 291 additions and 55 deletions
@@ -37,6 +37,12 @@ class SalesProspectController extends Controller
/** Стадии, по которым делать уже нечего — в фильтр «что надо сделать» не попадают. */
private const DEAD_STAGES = ['rejected', 'trash'];
/** Сроки режима «что надо сделать» — смотрят ВПЕРЁД плюс «просроченные». */
private const TODO_PERIODS = ['overdue', 'today', 'tomorrow', 'next7', 'next30', 'custom'];
/** Сроки режима «что менялось» — смотрят НАЗАД: будущего в истории не бывает. */
private const CHANGED_PERIODS = ['today', 'yesterday', 'd7', 'd30', 'custom'];
/** Все стадии в порядке колонок канбана. Должно совпадать с PROSPECT_STAGES (фронт). */
private const STAGES = [
'new', 'in_work', 'negotiation', 'manual_testing', 'registered', 'testing',
@@ -120,15 +126,21 @@ class SalesProspectController extends Controller
/**
* Фильтр доски по датам. Два режима, они взаимоисключающие по смыслу:
*
* todo «что надо сделать»: созвон попадает в период ИЛИ уже просрочен.
* Просроченное показывается ВСЕГДА, какой бы период ни выбрали:
* забытый звонок недельной давности важнее завтрашнего. Отказ и
* корзина не показываются по ним делать нечего.
* todo «что надо сделать»: срок созвона попадает в выбранный отрезок.
* Просроченное ОТДЕЛЬНЫЙ пункт срока (period=overdue), а не
* добавка к каждому: владелец просил, чтобы каждый пункт списка
* показывал ровно то, что написано (01.08.2026). Отказ и корзина
* не показываются никогда по ним делать нечего.
* changed «что менялось»: у карточки есть движение по стадиям ИЛИ запись
* в журнале разговоров за период. Движения включают автоматические
* (деньги тестирование/пополнил/пользователь) владелец просил
* считать ЛЮБОЕ движение, а не только разговоры.
*
* Сроки у режимов РАЗНЫЕ и не пересекаются: «что надо сделать» смотрит вперёд
* (просрочено / сегодня / завтра / ближайшие 730 дней), «что менялось» назад
* (сегодня / вчера / прошедшие 730 дней). Срок не из своего режима 422:
* «что менялось завтра» не бывает, и молча подменять его на месяц нельзя.
*
* Без date_mode доска отдаёт всё, как раньше.
*
* @param Builder<SalesProspect> $query
@@ -140,19 +152,27 @@ class SalesProspectController extends Controller
return;
}
$range = $this->resolvePeriod($request);
$kind = (string) $request->query('period', $mode === 'todo' ? 'today' : 'd30');
$this->assertPeriodFitsMode($mode, $kind);
if ($mode === 'todo') {
$query->whereNotIn('stage', self::DEAD_STAGES)
->whereNotNull('next_call_at')
->where(function ($q) use ($range) {
$q->whereBetween('next_call_at', [$range->start, $range->end])
->orWhere('next_call_at', '<', now());
});
$query->whereNotIn('stage', self::DEAD_STAGES)->whereNotNull('next_call_at');
// «Просроченные» — не отрезок календаря, а всё, что раньше «сейчас».
if ($kind === 'overdue') {
$query->where('next_call_at', '<', now());
return;
}
$range = $this->resolvePeriod($request, 'today');
$query->whereBetween('next_call_at', [$range->start, $range->end]);
return;
}
$range = $this->resolvePeriod($request);
$query->where(function ($q) use ($range) {
$q->whereExists(function ($sub) use ($range) {
$sub->selectRaw('1')
@@ -168,6 +188,28 @@ class SalesProspectController extends Controller
});
}
/**
* Срок должен подходить режиму: «что менялось завтра» и «просроченные за
* вчера» бессмыслица. Раньше чужой срок молча падал в резолвере на
* «текущий месяц», и доска показывала совсем не то, что выбрано.
*
* @throws ValidationException
*/
private function assertPeriodFitsMode(string $mode, string $kind): void
{
$allowed = $mode === 'todo'
? self::TODO_PERIODS
: self::CHANGED_PERIODS;
if (in_array($kind, $allowed, true)) {
return;
}
throw ValidationException::withMessages([
'period' => 'Этот срок не подходит к выбранному фильтру по датам.',
]);
}
/**
* Менеджер заводит СВОЕГО кандидата (инициатива, не из поиска).
* Карточка всегда создаётся автору (sales_user_id из тела игнорируется),
@@ -26,15 +26,16 @@ trait ResolvesSalesPeriod
* Период из query-параметров запроса.
*
* По умолчанию последние 30 дней: тот же период, что PeriodPicker
* показывает при первом заходе.
* показывает при первом заходе. Экраны, где «назад на 30 дней» бессмысленно
* (воронка «что надо сделать» смотрит вперёд), передают свой $default.
*
* @throws ValidationException при неполном или перевёрнутом произвольном периоде
*/
protected function resolvePeriod(Request $request): SalesPeriodRange
protected function resolvePeriod(Request $request, string $default = 'd30'): SalesPeriodRange
{
try {
return app(SalesPeriodResolver::class)->resolve([
'kind' => (string) $request->query('period', 'd30'),
'kind' => (string) $request->query('period', $default),
'from' => $request->query('from'),
'to' => $request->query('to'),
]);
@@ -16,6 +16,8 @@ use InvalidArgumentException;
* 'yesterday' вчера.
* 'd7' последние 7 дней, включая сегодня.
* 'd30' последние 30 дней, включая сегодня.
* 'next7' ближайшие 7 дней, включая сегодня (вперёд).
* 'next30' ближайшие 30 дней, включая сегодня (вперёд).
* 'this' текущий месяц целиком.
* 'prev' предыдущий месяц целиком.
* 'prev2' месяц перед предыдущим целиком.
@@ -45,6 +47,8 @@ final class SalesPeriodResolver
'yesterday' => $this->dayRange(-1),
'd7' => $this->lastDays(7),
'd30' => $this->lastDays(30),
'next7' => $this->nextDays(7),
'next30' => $this->nextDays(30),
'prev' => $this->monthRange(-1),
'prev2' => $this->monthRange(-2),
'custom' => $this->customRange($period),
@@ -88,6 +92,22 @@ final class SalesPeriodResolver
);
}
/**
* Ближайшие $days дней, ВКЛЮЧАЯ сегодня: next7 = сегодня и 6 следующих.
*
* Зеркало lastDays() для фильтра «что надо сделать»: там срок смотрит
* ВПЕРЁД (когда звонить), а не назад (что уже случилось).
*/
private function nextDays(int $days): SalesPeriodRange
{
$today = CarbonImmutable::now(self::TZ)->startOfDay();
return new SalesPeriodRange(
$today,
$today->addDays($days - 1)->setTime(23, 59, 59),
);
}
/** Один день, смещённый на $offset от сегодняшнего (0 — сегодня, -1 — вчера). */
private function dayRange(int $offset): SalesPeriodRange
{
+1 -1
View File
@@ -5067,7 +5067,7 @@ parameters:
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
identifier: method.notFound
count: 9
count: 14
path: tests/Feature/Sales/SalesProspectDateFilterTest.php
-
+19 -4
View File
@@ -1302,13 +1302,28 @@ export interface ProspectsResponse {
/**
* Фильтр доски по датам:
* todo — что надо сделать в этот срок (плюс всё просроченное);
* changed — что менялось в этот срок (любое движение карточки).
* todo — что надо сделать в этот срок (срок смотрит вперёд, плюс отдельный
* пункт «Просроченные»);
* changed — что менялось в этот срок (срок смотрит назад, любое движение карточки).
*/
export type ProspectDateMode = 'todo' | 'changed';
/** Вид периода — те же, что понимает SalesPeriodResolver на сервере. */
export type ProspectPeriodKind = 'today' | 'tomorrow' | 'yesterday' | 'd7' | 'd30' | 'custom';
/**
* Вид периода — те же, что понимает SalesPeriodResolver на сервере.
* Наборы у режимов разные: overdue/tomorrow/next7/next30 — только для «что надо
* сделать», yesterday/d7/d30 — только для «что менялось». Пара из чужого набора
* получает от сервера 422 (SalesProspectController::assertPeriodFitsMode).
*/
export type ProspectPeriodKind =
| 'overdue'
| 'today'
| 'tomorrow'
| 'next7'
| 'next30'
| 'yesterday'
| 'd7'
| 'd30'
| 'custom';
export interface ProspectDateFilterValue {
mode: ProspectDateMode | null;
@@ -2,11 +2,17 @@
/**
* Фильтр воронки по датам. Два режима, взаимоисключающие по смыслу:
*
* «Что надо сделать» — утром: по кому сегодня/завтра звонить, плюс всё
* просроченное (забытый звонок важнее завтрашнего).
* «Что менялось» — вечером: что я сделал за день или за период. Считается
* ЛЮБОЕ движение карточки, включая автоматическое
* (потратил деньги, пополнил счёт, стал клиентом).
* «Что надо сделать» — утром: по кому звонить. Срок смотрит ВПЕРЁД, и первым
* пунктом идут «Просроченные» — то, что уже пропустили.
* «Что менялось» — вечером: что я сделал за день или за период. Срок
* смотрит НАЗАД. Считается ЛЮБОЕ движение карточки,
* включая автоматическое (потратил деньги, пополнил
* счёт, стал клиентом).
*
* Списки сроков у режимов РАЗНЫЕ и намеренно не пересекаются (01.08.2026):
* «завтра» в истории не бывает, «вчера» в планах — это просто «просроченное».
* Сервер такую пару отвергает с 422, чтобы никакой пункт не показывал молча
* не то, что на нём написано.
*
* Владелец назвал это «двумя фильтрами», но сделан один орган с выбором режима:
* одновременно они не нужны никогда, а два независимых органа пересекались бы
@@ -16,7 +22,7 @@
* отмечены ОБЕ даты: на половинчатом диапазоне сервер отвечает ошибкой.
* Тот же приём, что в PeriodPicker.
*/
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import type { ProspectDateFilterValue, ProspectDateMode, ProspectPeriodKind } from '../../api/sales';
const emit = defineEmits<{ change: [value: ProspectDateFilterValue] }>();
@@ -35,15 +41,27 @@ const modeItems: { value: ProspectDateMode | null; title: string }[] = [
{ value: 'changed', title: 'Что менялось' },
];
const periodItems: { value: ProspectPeriodKind; title: string }[] = [
/** Планы — вперёд, и «Просроченные» первым пунктом (просьба владельца). */
const TODO_PERIODS: { value: ProspectPeriodKind; title: string }[] = [
{ value: 'overdue', title: 'Просроченные' },
{ value: 'today', title: 'Сегодня' },
{ value: 'tomorrow', title: 'Завтра' },
{ value: 'next7', title: 'Ближайшие 7 дней' },
{ value: 'next30', title: 'Ближайшие 30 дней' },
{ value: 'custom', title: 'Произвольный период' },
];
/** История — назад. «Завтра» здесь нет: будущего в истории не бывает. */
const CHANGED_PERIODS: { value: ProspectPeriodKind; title: string }[] = [
{ value: 'today', title: 'Сегодня' },
{ value: 'yesterday', title: 'Вчера' },
{ value: 'd7', title: '7 дней' },
{ value: 'd30', title: '30 дней' },
{ value: 'custom', title: 'Произвольный период' },
];
const periodItems = computed(() => (mode.value === 'todo' ? TODO_PERIODS : CHANGED_PERIODS));
/** Дата из календаря (Date или строка) → YYYY-MM-DD по местному времени. */
function toIsoDay(value: unknown): string | null {
const d = value instanceof Date ? value : new Date(String(value));
@@ -113,10 +131,6 @@ defineExpose({ mode, period, customRange, applyCustom, modeItems, periodItems, c
-->
<div class="d-flex align-center ga-3" role="group" aria-label="Фильтр по датам">
<template v-if="mode">
<span v-if="mode === 'todo'" class="text-caption text-medium-emphasis text-no-wrap">
плюс всё просроченное
</span>
<!-- Календарь для произвольного периода: отмечаются начало и конец. -->
<v-menu v-model="calendarOpen" :close-on-content-click="false" location="bottom end">
<template #activator="{ props: activator }">
@@ -50,7 +50,7 @@ function df_names(TestResponse $res): array
// ── режим «что надо сделать» ──────────────────────────────────────────────────
test('что надо сделать на сегодня: сегодняшний созвон и просроченный, но не завтрашний', function () {
test('что надо сделать на сегодня: только сегодняшний созвон', function () {
$mgr = df_user();
df_card($mgr, 'СЕГОДНЯ', 'negotiation', '2026-06-15T14:00:00');
df_card($mgr, 'ПРОСРОЧЕН', 'negotiation', '2026-06-10T14:00:00');
@@ -61,10 +61,10 @@ test('что надо сделать на сегодня: сегодняшний
->getJson('/api/sales/prospects?date_mode=todo&period=today');
$res->assertOk();
expect(df_names($res))->toBe(['ПРОСРОЧЕН', 'СЕГОДНЯ']);
expect(df_names($res))->toBe(['СЕГОДНЯ']);
});
test('что надо сделать на завтра: завтрашний созвон И просроченное — забытое важнее', function () {
test('что надо сделать на завтра: только завтрашний созвон', function () {
$mgr = df_user();
df_card($mgr, 'ЗАВТРА', 'negotiation', '2026-06-16T14:00:00');
df_card($mgr, 'ПРОСРОЧЕН', 'negotiation', '2026-06-10T14:00:00');
@@ -74,7 +74,63 @@ test('что надо сделать на завтра: завтрашний с
->getJson('/api/sales/prospects?date_mode=todo&period=tomorrow');
$res->assertOk();
expect(df_names($res))->toBe(['ЗАВТРА', 'ПРОСРОЧЕН']);
expect(df_names($res))->toBe(['ЗАВТРА']);
});
test('просроченные: всё, чей срок раньше «сейчас», и ничего из будущего', function () {
$mgr = df_user();
df_card($mgr, 'ЗАБЫТ ДАВНО', 'negotiation', '2026-05-30T14:00:00');
df_card($mgr, 'ПРОСРОЧЕН УТРОМ', 'negotiation', '2026-06-15T05:00:00');
df_card($mgr, 'СЕГОДНЯ ПОЗЖЕ', 'negotiation', '2026-06-15T18:00:00');
df_card($mgr, 'ЗАВТРА', 'negotiation', '2026-06-16T14:00:00');
df_card($mgr, 'БЕЗ ДАТЫ', 'in_work');
$res = $this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=todo&period=overdue');
$res->assertOk();
expect(df_names($res))->toBe(['ЗАБЫТ ДАВНО', 'ПРОСРОЧЕН УТРОМ']);
});
test('ближайшие 7 дней смотрят ВПЕРЁД, а не назад', function () {
$mgr = df_user();
df_card($mgr, 'СЕГОДНЯ', 'negotiation', '2026-06-15T14:00:00');
df_card($mgr, 'ЧЕРЕЗ 6 ДНЕЙ', 'negotiation', '2026-06-21T14:00:00');
df_card($mgr, 'ЧЕРЕЗ 8 ДНЕЙ', 'negotiation', '2026-06-23T14:00:00');
df_card($mgr, 'НЕДЕЛЮ НАЗАД', 'negotiation', '2026-06-09T14:00:00');
$res = $this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=todo&period=next7');
$res->assertOk();
expect(df_names($res))->toBe(['СЕГОДНЯ', 'ЧЕРЕЗ 6 ДНЕЙ']);
});
test('ближайшие 30 дней тоже вперёд', function () {
$mgr = df_user();
df_card($mgr, 'ЧЕРЕЗ 29 ДНЕЙ', 'negotiation', '2026-07-14T14:00:00');
df_card($mgr, 'ЧЕРЕЗ 31 ДЕНЬ', 'negotiation', '2026-07-16T14:00:00');
df_card($mgr, 'МЕСЯЦ НАЗАД', 'negotiation', '2026-05-16T14:00:00');
$res = $this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=todo&period=next30');
$res->assertOk();
expect(df_names($res))->toBe(['ЧЕРЕЗ 29 ДНЕЙ']);
});
test('срок не из своего режима — отказ 422, а не молчаливая подмена периодом', function () {
$mgr = df_user();
$this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=changed&period=tomorrow')
->assertStatus(422)
->assertJsonValidationErrors('period');
$this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=todo&period=yesterday')
->assertStatus(422)
->assertJsonValidationErrors('period');
});
test('что надо сделать: отказ и корзина не показываются даже с просроченной датой', function () {
@@ -86,7 +142,7 @@ test('что надо сделать: отказ и корзина не пока
df_card($mgr, 'ЖИВАЯ', 'negotiation', '2026-06-10T14:00:00');
$res = $this->actingAs($mgr, 'sales')
->getJson('/api/sales/prospects?date_mode=todo&period=today');
->getJson('/api/sales/prospects?date_mode=todo&period=overdue');
$res->assertOk();
expect(df_names($res))->toBe(['ЖИВАЯ']);
+46 -3
View File
@@ -32,9 +32,52 @@ describe('ProspectDateFilter', () => {
expect(values).toEqual([null, 'todo', 'changed']);
});
it('в периодах есть «завтра» — по нему смотрят, что делать назавтра', () => {
const { vm } = factory();
expect(vm.periodItems.map((i) => i.value)).toContain('tomorrow');
it('в планах: «просроченные» первым пунктом, «завтра» есть, «вчера» нет', async () => {
const { w, vm } = factory();
vm.mode = 'todo';
await w.vm.$nextTick();
const values = vm.periodItems.map((i) => i.value);
expect(values[0]).toBe('overdue');
expect(values).toEqual(['overdue', 'today', 'tomorrow', 'next7', 'next30', 'custom']);
expect(values).not.toContain('yesterday');
});
it('в истории нет ни «завтра», ни «просроченных» — будущего в истории не бывает', async () => {
const { w, vm } = factory();
vm.mode = 'changed';
await w.vm.$nextTick();
const values = vm.periodItems.map((i) => i.value);
expect(values).toEqual(['today', 'yesterday', 'd7', 'd30', 'custom']);
expect(values).not.toContain('tomorrow');
expect(values).not.toContain('overdue');
});
it('«7 дней» в планах — вперёд (next7), в истории — назад (d7)', async () => {
const { w, vm } = factory();
vm.mode = 'todo';
await w.vm.$nextTick();
expect(vm.periodItems.find((i) => i.value === 'next7')?.title).toBe('Ближайшие 7 дней');
vm.mode = 'changed';
await w.vm.$nextTick();
expect(vm.periodItems.find((i) => i.value === 'd7')?.title).toBe('7 дней');
});
it('после смены режима выбранный срок остаётся допустимым для нового режима', async () => {
const { w, vm } = factory();
vm.mode = 'todo';
await w.vm.$nextTick();
vm.period = 'next7';
await w.vm.$nextTick();
vm.mode = 'changed';
await w.vm.$nextTick();
// Иначе наружу ушла бы пара «что менялось + ближайшие 7 дней» → 422 с сервера.
expect(vm.periodItems.map((i) => i.value)).toContain(vm.period);
expect(w.emitted('change')?.at(-1)?.[0]).toMatchObject({ mode: 'changed', period: 'today' });
});
it('выбор режима показывает период и сообщает наружу', async () => {
@@ -2,10 +2,20 @@ import { describe, it, expect } from 'vitest';
import { AD_CHANNELS } from '../../resources/js/components/layout/advertisingChannels';
describe('AD_CHANNELS', () => {
it('yandex audience is a real route now, others remain stubs', () => {
const yandex = AD_CHANNELS.find((c) => c.testid === 'ad-nav-yandex-audience');
expect(yandex?.route).toBe('/advertising/yandex');
const stubs = AD_CHANNELS.filter((c) => c.testid !== 'ad-nav-yandex-audience');
// Живых площадок стало две: к Яндекс.Аудитории добавился Телеграм
// (модуль телеграм-рекламы). Тест перечисляет их поимённо, чтобы новая
// заглушка с случайным роутом не проехала молча.
it('живые площадки — Яндекс и Телеграм, остальные пока заглушки', () => {
const live: Record<string, string> = {
'ad-nav-yandex-audience': '/advertising/yandex',
'ad-nav-telegram': '/advertising/telegram',
};
for (const [testid, route] of Object.entries(live)) {
expect(AD_CHANNELS.find((c) => c.testid === testid)?.route).toBe(route);
}
const stubs = AD_CHANNELS.filter((c) => !(c.testid in live));
expect(stubs.every((c) => !c.route)).toBe(true);
});
});
@@ -147,6 +147,20 @@ it('kind=tomorrow — только завтрашний день', function ():
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-16 23:59:59');
});
it('kind=next7 — ближайшие 7 дней ВПЕРЁД, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'next7']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-15 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-21 23:59:59');
});
it('kind=next30 — ближайшие 30 дней ВПЕРЁД, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'next30']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-15 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-07-14 23:59:59');
});
it('kind=d7 — последние 7 дней, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'd7']);
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@@ -145,20 +145,36 @@ Append-only, как и журнал разговоров: движение —
Что менялось
```
Второй список — период: **Сегодня / Завтра / Вчера / 7 дней / 30 дней / Произвольный
период** (календарь-диапазон). Он появляется, только когда выбран режим.
Второй список — срок. Он появляется, только когда выбран режим, и **у каждого режима
свой набор** (уточнение владельца 01.08.2026, вечер):
| «Что надо сделать» (вперёд) | «Что менялось» (назад) |
|---|---|
| **Просроченные** | Сегодня |
| Сегодня | Вчера |
| Завтра | 7 дней |
| Ближайшие 7 дней | 30 дней |
| Ближайшие 30 дней | Произвольный период |
| Произвольный период | |
Наборы намеренно не пересекаются: «что менялось завтра» не бывает, а «планы на вчера» —
это и есть «просроченное». Пару из чужого набора сервер отвергает с 422, а не подменяет
молча другим периодом (`SalesProspectController::assertPeriodFitsMode`).
Одинаковый орган и у менеджера, и у начальника — компонент один.
### 8.2. Режим «Что надо сделать»
Карточки, по которым в выбранный срок надо звонить или что-то делать:
Карточки, по которым в выбранный срок надо звонить или что-то делать: `next_call_at`
попадает в выбранный отрезок. **Каждый пункт показывает ровно то, что на нём написано**
«Сегодня» это только сегодняшние созвоны, ничего лишнего.
`next_call_at` попадает в период **ИЛИ** `next_call_at` уже просрочен (`< сейчас`).
Просроченное — **отдельный первый пункт** списка (`period=overdue`): всё, чей срок раньше
«сейчас». Раньше просроченное подмешивалось в любой выбранный период, и «Сегодня»
показывало не только сегодняшнее — владелец попросил развести (01.08.2026).
Просроченные показываются **всегда**, каким бы период ни выбрали — владелец просил их
отдельно («и что просрочено»), и забытый звонок недельной давности важнее завтрашнего.
Они и так подсвечены красным на доске.
Сроки этого режима смотрят **вперёд**: «Ближайшие 7 дней» — сегодня и 6 следующих
(`next7`), а не прошедшая неделя. Иначе «что надо сделать» показывало бы прошлое.
Из выборки исключены стадии `rejected` и `trash` — по ним делать нечего. Карточки без
даты созвона в этот режим не попадают: делать по ним нечего в конкретный срок.
@@ -177,9 +193,12 @@ Append-only, как и журнал разговоров: движение —
### 8.4. Границы дат
Периоды считаются в МСК существующим `SalesPeriodResolver` — тем же, что «30 дней»
на других экранах портала. К нему добавляется вид `tomorrow` (владелец назвал «завтра»
прямо). Приложение живёт в UTC, менеджер думает в МСК — своей арифметики дат не заводим,
это уже решённая в проекте задача.
на других экранах портала. К нему добавлены виды `tomorrow` (владелец назвал «завтра»
прямо) и зеркала `next7` / `next30` — те же 7 и 30 дней, но вперёд. Приложение живёт
в UTC, менеджер думает в МСК — своей арифметики дат не заводим, это уже решённая
в проекте задача.
`overdue` резолвером не считается вовсе: это не отрезок календаря, а «раньше сейчас».
## 9. Что отрезано (осознанно)
@@ -199,6 +218,8 @@ Append-only, как и журнал разговоров: движение —
2. Платящему клиенту результат «В корзину» не предлагается.
3. «Выслано КП» сохраняется **без** даты и **с** датой.
4. После «ручное тестирование → отказ» в шапке «Отказа» появляется дробь.
5. Фильтр «Что надо сделать» на «Сегодня» показывает сегодняшние созвоны и просроченные.
6. Фильтр «Что менялось» на «Сегодня» показывает карточку, которую только что подвинули.
7. Всё то же самое в «Воронке отдела» у начальника.
5. Фильтр «Что надо сделать» на «Сегодня» показывает **только** сегодняшние созвоны,
на «Просроченные» — только забытые, на «Завтра» — только завтрашние.
6. В сроках «Что надо сделать» нет «Вчера», в сроках «Что менялось» нет «Завтра».
7. Фильтр «Что менялось» на «Сегодня» показывает карточку, которую только что подвинули.
8. Всё то же самое в «Воронке отдела» у начальника.