feat(projects): leadDate util + source-lock поля в типе Project
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 июня');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user