feat(projects): leadDate util + source-lock поля в типе Project

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-22 20:54:28 +03:00
parent 08cf23893a
commit 1b757a8d67
3 changed files with 52 additions and 0 deletions
+4
View File
@@ -19,6 +19,10 @@ export interface Project {
delivery_days_mask?: number;
sync_status: 'ok' | 'pending' | 'failed';
last_synced_at?: string | null;
// Блокировка смены источника (спека 2026-06-22-project-source-edit-lock-ux).
source_locked?: boolean;
source_unlock_at?: string | null;
source_unlock_projected?: boolean;
}
export const useProjectsStore = defineStore('projects', () => {
+24
View File
@@ -0,0 +1,24 @@
/**
* Форматирование дат для UI блокировки источника и баннера нового проекта.
* Спека: docs/superpowers/specs/2026-06-22-project-source-edit-lock-ux-design.md.
*/
/** ISO-строку → «23 июня» (ru). Пусто/невалид → ''. */
export function formatLeadDate(iso: string | null | undefined): string {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(d);
}
/**
* Дата старта лидов нового проекта по правилу слепка 18:00 МСК:
* создал до 18:00 МСК → лиды с завтра; после 18:00 → с послезавтра.
* РФ круглый год UTC+3 (без перехода на летнее время).
*/
export function firstLeadDate(now: Date = new Date()): string {
const msk = new Date(now.getTime() + 3 * 3600 * 1000);
const addDays = msk.getUTCHours() >= 18 ? 2 : 1;
const target = new Date(Date.UTC(msk.getUTCFullYear(), msk.getUTCMonth(), msk.getUTCDate() + addDays));
return new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', timeZone: 'UTC' }).format(target);
}
+24
View File
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { formatLeadDate, firstLeadDate } from '../../resources/js/utils/leadDate';
describe('formatLeadDate', () => {
it('formats ISO to "D MMMM" in Russian', () => {
expect(formatLeadDate('2026-06-23T21:00:00+03:00')).toBe('23 июня');
});
it('returns empty string for null/invalid', () => {
expect(formatLeadDate(null)).toBe('');
expect(formatLeadDate('')).toBe('');
expect(formatLeadDate('not-a-date')).toBe('');
});
});
describe('firstLeadDate (порог 18:00 МСК)', () => {
it('до 18:00 МСК → завтра', () => {
// 2026-06-22 12:00 UTC = 15:00 МСК → завтра 23 июня
expect(firstLeadDate(new Date('2026-06-22T12:00:00Z'))).toBe('23 июня');
});
it('после 18:00 МСК → послезавтра', () => {
// 2026-06-22 16:00 UTC = 19:00 МСК → послезавтра 24 июня
expect(firstLeadDate(new Date('2026-06-22T16:00:00Z'))).toBe('24 июня');
});
});