fix: даты «только дата» больше не сдвигаются на день назад
Tests / frontend-test (push) Successful in 2m1s
Tests / frontend-test (push) Successful in 2m1s
SharePoint хранит поле «только дата» как полночь по поясу сайта (UTC+5) в UTC: «05.10» приходит из REST как 2026-10-04T19:00:00Z. Формы брали первые 10 символов и показывали 04.10, а каждое пересохранение (полное право, исполнитель, повторная отправка) записывало дату ещё на день раньше. Чтение всех дат идёт через spDateOnlyToIso (округление до ближайшей полуночи UTC), «сегодня» — через todayIsoDate вместо toISOString, который до 05:00 давал вчерашнее число. Уже сдвинутые записи в SP не исправляются. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
import { parseTasks } from './tripData';
|
||||
import { parseRoute } from './route';
|
||||
@@ -42,8 +43,7 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string):
|
||||
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
// Колонка «только дата» — время в ответе нулевое, берём дату как есть.
|
||||
SubmitDate: typeof item.SubmitDate === 'string' ? item.SubmitDate.slice(0, 10) : '',
|
||||
SubmitDate: spDateOnlyToIso(item.SubmitDate),
|
||||
Employee: employee?.EMail || '',
|
||||
Position: (item.Position as string) || '',
|
||||
Organization: (item.Organization as string) || '',
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import { todayIsoDate } from '../../shared/dateUtils';
|
||||
import {
|
||||
approvalListTitle,
|
||||
approvalListTitleForListId,
|
||||
@@ -109,7 +110,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
|
||||
if (editId) {
|
||||
return;
|
||||
}
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const today = todayIsoDate();
|
||||
setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today }));
|
||||
}, [editId]);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
/** Число или '' (SharePoint отдаёт null для незаполненного Number). */
|
||||
@@ -60,7 +61,7 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
|
||||
Employee: employee?.EMail || '',
|
||||
Position: (item.Position as string) || '',
|
||||
Department: (item.Department as string) || '',
|
||||
TerminationDate: termination.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
TerminationDate: spDateOnlyToIso(termination),
|
||||
ParkingCard: !!item.ParkingCard,
|
||||
AccessCard: !!item.AccessCard,
|
||||
// Хранится в WriteOffTrainingAmount, не в одноимённом TrainingAmount — см.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, IConcreteItem, EMPTY_DATA } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -53,7 +54,7 @@ export async function loadConcreteRequest(itemId: number, listId?: string): Prom
|
||||
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
RequestDate: requestDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
RequestDate: spDateOnlyToIso(requestDate),
|
||||
SiteChiefEmail: (item.SiteChiefEmail as string) || '',
|
||||
SiteChiefName: (item.SiteChiefName as string) || '',
|
||||
ForemanEmail: (item.ForemanEmail as string) || '',
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
dataListTitleForListId
|
||||
} from '../../shared/listTitles';
|
||||
import { requestLinkUrl } from '../../shared/routing';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID as CLEARANCE_FORM_ID, EMPTY_DATA as CLEARANCE_EMPTY, IFormData as IClearanceData } from '../Clearance/model';
|
||||
import { clearanceApprovalService, saveClearanceRequest } from '../Clearance/submit';
|
||||
import { FORM_ID, primaryPosition, zipOrganization } from './model';
|
||||
@@ -136,7 +137,7 @@ export async function ensureClearanceForDismissal(
|
||||
Position: position,
|
||||
Department: department,
|
||||
// Последний рабочий день заявления = дата расторжения договора в обходном листе.
|
||||
TerminationDate: ((item.LastWorkingDay as string) || '').slice(0, 10),
|
||||
TerminationDate: spDateOnlyToIso(item.LastWorkingDay),
|
||||
Organization: organizationText
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, zipOrganization } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -47,7 +48,7 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
Employee: employee?.EMail || '',
|
||||
LastWorkingDay: lastDay.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
LastWorkingDay: spDateOnlyToIso(lastDay),
|
||||
Phone: (item.Phone as string) || '',
|
||||
Comment: (item.Comment as string) || '',
|
||||
Organization: zipOrganization(item.Organization, item.Department, item.Position),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -47,7 +48,7 @@ export async function loadEmployeeTransferRequest(itemId: number, listId?: strin
|
||||
Employee: employee?.EMail || '',
|
||||
Position: (item.Position as string) || '',
|
||||
Organization: (item.Organization as string) || '',
|
||||
TransferDate: transferDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
TransferDate: spDateOnlyToIso(transferDate),
|
||||
NewPosition: (item.NewPosition as string) || '',
|
||||
NewDepartment: (item.NewDepartment as string) || '',
|
||||
NewDepartmentId: (item.NewDepartmentId as string) || '',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, ICompanyShare, LEGACY_HIRE_TYPES } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
@@ -76,7 +77,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
||||
if (value === null || value === undefined) {
|
||||
writable[key] = '';
|
||||
} else if (key === 'HireHireDate' && typeof value === 'string') {
|
||||
writable[key] = value.slice(0, 10); // ISO → YYYY-MM-DD для input type=date
|
||||
writable[key] = spDateOnlyToIso(value);
|
||||
} else {
|
||||
writable[key] = String(value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, ILanguageLevel } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
@@ -103,7 +104,7 @@ export async function loadSelectionRequest(itemId: number, listId?: string): Pro
|
||||
if (value === null || value === undefined) {
|
||||
writable[key] = '';
|
||||
} else if (DATE_FIELDS.indexOf(key) !== -1 && typeof value === 'string') {
|
||||
writable[key] = value.slice(0, 10); // ISO → YYYY-MM-DD для input type=date
|
||||
writable[key] = spDateOnlyToIso(value);
|
||||
} else {
|
||||
writable[key] = String(value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
||||
@@ -72,7 +73,7 @@ export async function loadReferralRequest(itemId: number, listId?: string): Prom
|
||||
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
SubmitDate: submitDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
SubmitDate: spDateOnlyToIso(submitDate),
|
||||
Referrer: referrer?.EMail || '',
|
||||
ReferrerPosition: (item.ReferrerPosition as string) || '',
|
||||
ReferrerDepartment: (item.ReferrerDepartment as string) || '',
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import { todayIsoDate } from '../../shared/dateUtils';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import {
|
||||
approvalListTitle,
|
||||
@@ -82,7 +83,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
||||
return;
|
||||
}
|
||||
const user = context.pageContext.user;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const today = todayIsoDate();
|
||||
const requesterEmail = effectiveUserEmail(context);
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { parseGroupedJson } from './cost';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, EMPTY_ANNEX_ROW, IAnnexRow } from './model';
|
||||
|
||||
@@ -57,7 +58,7 @@ function parseRows<T>(raw: unknown): T[] {
|
||||
|
||||
/** ISO-дата из SharePoint → YYYY-MM-DD для input type=date. */
|
||||
function toDateInput(raw: unknown): string {
|
||||
return typeof raw === 'string' ? raw.slice(0, 10) : '';
|
||||
return spDateOnlyToIso(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
import { parseMissedDays } from './missedDaysJson';
|
||||
import { toDateKey } from './skud/skudDays';
|
||||
@@ -52,8 +53,7 @@ export async function loadSkudExplanationRequest(itemId: number, listId?: string
|
||||
|
||||
const data: IFormData = {
|
||||
...EMPTY_DATA,
|
||||
// Колонка «только дата» — время в ответе нулевое, берём дату как есть (как в Referral/load.ts).
|
||||
SubmitDate: ((item.SubmitDate as string) || '').slice(0, 10),
|
||||
SubmitDate: spDateOnlyToIso(item.SubmitDate),
|
||||
Employee: employee?.EMail || '',
|
||||
EmployeeFio: (item.SkudEmployeeFio as string) || employee?.Title || '',
|
||||
Position: (item.Position as string) || '',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { HOLIDAYS_LIST_TITLE, HOLIDAYS_SITE_URL } from '../../../shared/config';
|
||||
import { spDateOnlyToIso } from '../../../shared/dateUtils';
|
||||
|
||||
interface IHolidayItem {
|
||||
Title?: string;
|
||||
@@ -25,7 +26,7 @@ async function fetchHolidays(context: WebPartContext): Promise<Map<string, strin
|
||||
const body = (await response.json()) as { value?: IHolidayItem[] };
|
||||
const map = new Map<string, string>();
|
||||
(body.value || []).forEach((item) => {
|
||||
if (item.holidayDate) map.set(item.holidayDate.slice(0, 10), stripParens(item.Title || ''));
|
||||
if (item.holidayDate) map.set(spDateOnlyToIso(item.holidayDate), stripParens(item.Title || ''));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { requestLinkUrl } from '../../shared/routing';
|
||||
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
||||
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import { todayIsoDate } from '../../shared/dateUtils';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import {
|
||||
approvalListTitle,
|
||||
@@ -120,7 +121,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
return;
|
||||
}
|
||||
const user = context.pageContext.user;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const today = todayIsoDate();
|
||||
const requesterEmail = effectiveUserEmail(context);
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ILineItem } from '../../shared/ui';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -70,7 +71,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
||||
Department: (item.Department as string) || '',
|
||||
DepartmentId: (item.DepartmentId as string) || '',
|
||||
Subject: (item.Subject as string) || '',
|
||||
RequestDate: requestDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
RequestDate: spDateOnlyToIso(requestDate),
|
||||
SourceProject: (item.SourceProject as string) || '',
|
||||
SourceProjectId: (item.SourceProjectId as string) || '',
|
||||
SourceProjectManager: (item.SourceProjectManager as string) || '',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -51,8 +52,8 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
||||
...EMPTY_DATA,
|
||||
Employee: employee?.EMail || '',
|
||||
Position: (item.Position as string) || '',
|
||||
StartDate: start.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
EndDate: end.slice(0, 10),
|
||||
StartDate: spDateOnlyToIso(start),
|
||||
EndDate: spDateOnlyToIso(end),
|
||||
Reason: (item.Reason as string) || '',
|
||||
Substitute: substitute?.EMail || '',
|
||||
Organization: (item.Organization as string) || '',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
export interface ILoadedRequest {
|
||||
@@ -50,7 +51,7 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
|
||||
...EMPTY_DATA,
|
||||
Employee: employee?.EMail || '',
|
||||
Position: (item.Position as string) || '',
|
||||
StartDate: start.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
StartDate: spDateOnlyToIso(start),
|
||||
DaysCount: days === null || days === undefined ? '' : String(days),
|
||||
Substitute: substitute?.EMail || '',
|
||||
Organization: (item.Organization as string) || '',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||
|
||||
@@ -52,7 +53,7 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro
|
||||
if (value === null || value === undefined) {
|
||||
writable[key] = '';
|
||||
} else if (key === 'StartDate' && typeof value === 'string') {
|
||||
writable[key] = value.slice(0, 10);
|
||||
writable[key] = spDateOnlyToIso(value);
|
||||
} else {
|
||||
writable[key] = String(value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { spDateOnlyToIso } from './dateUtils';
|
||||
|
||||
// SharePoint хранит поле «только дата» как полночь по поясу САЙТА, переведённую
|
||||
// в UTC, — поэтому «5 октября» приходит из REST как предыдущий день по UTC.
|
||||
describe('spDateOnlyToIso', () => {
|
||||
it('сайт в UTC+5 (Астана с 2024): 04T19:00Z → 5-е', () => {
|
||||
expect(spDateOnlyToIso('2026-10-04T19:00:00Z')).toBe('2026-10-05');
|
||||
});
|
||||
|
||||
it('сайт в UTC+6 (старый пояс Астаны): 04T18:00Z → 5-е', () => {
|
||||
expect(spDateOnlyToIso('2026-10-04T18:00:00Z')).toBe('2026-10-05');
|
||||
});
|
||||
|
||||
it('сайт в UTC: 05T00:00Z → 5-е', () => {
|
||||
expect(spDateOnlyToIso('2026-10-05T00:00:00Z')).toBe('2026-10-05');
|
||||
});
|
||||
|
||||
it('переход через месяц и год', () => {
|
||||
expect(spDateOnlyToIso('2026-09-30T19:00:00Z')).toBe('2026-10-01');
|
||||
expect(spDateOnlyToIso('2026-12-31T19:00:00Z')).toBe('2027-01-01');
|
||||
});
|
||||
|
||||
it('високосный год: 28 февраля → 29-е', () => {
|
||||
expect(spDateOnlyToIso('2028-02-28T19:00:00Z')).toBe('2028-02-29');
|
||||
});
|
||||
|
||||
it('строку без времени возвращает как есть', () => {
|
||||
expect(spDateOnlyToIso('2026-10-05')).toBe('2026-10-05');
|
||||
});
|
||||
|
||||
it('пустое/отсутствующее значение → пустая строка', () => {
|
||||
expect(spDateOnlyToIso('')).toBe('');
|
||||
expect(spDateOnlyToIso(null)).toBe('');
|
||||
expect(spDateOnlyToIso(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('повторное сохранение не сдвигает дату (запись → чтение → запись)', () => {
|
||||
// Имитация: SP вернул 04T19:00Z, мы прочитали, сохранили обратно «как есть»,
|
||||
// SP снова хранит 04T19:00Z — дата обязана остаться 5-м, а не уехать на 4-е.
|
||||
const firstRead = spDateOnlyToIso('2026-10-04T19:00:00Z');
|
||||
expect(firstRead).toBe('2026-10-05');
|
||||
const secondRead = spDateOnlyToIso(`${addDays(firstRead, -1)}T19:00:00Z`);
|
||||
expect(secondRead).toBe(firstRead);
|
||||
});
|
||||
});
|
||||
|
||||
function addDays(isoDate: string, days: number): string {
|
||||
const d = new Date(`${isoDate}T00:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
@@ -25,6 +25,30 @@ export function isoDatePlusMonths(months: number): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
/** Сегодня по МЕСТНОМУ времени в формате YYYY-MM-DD (toISOString дал бы дату по UTC — до 05:00 вчерашнюю). */
|
||||
export function todayIsoDate(): string {
|
||||
return isoDatePlusDays(0);
|
||||
}
|
||||
|
||||
const HALF_DAY_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Значение поля SharePoint «только дата» → YYYY-MM-DD для input type=date.
|
||||
*
|
||||
* REST отдаёт такие поля в UTC: «5 октября» на сайте с поясом UTC+5 приходит
|
||||
* как «2026-10-04T19:00:00Z», и `slice(0, 10)` давал 4-е число (а каждое
|
||||
* пересохранение сдвигало дату ещё на день назад). Округляем до ближайшей
|
||||
* полуночи UTC (+12 ч) — так верно для любого пояса сайта в пределах ±11 ч и
|
||||
* не зависит от пояса браузера.
|
||||
*/
|
||||
export function spDateOnlyToIso(value: unknown): string {
|
||||
if (typeof value !== 'string' || value === '') return '';
|
||||
if (value.indexOf('T') < 0) return value.slice(0, 10);
|
||||
const ms = Date.parse(value);
|
||||
if (isNaN(ms)) return value.slice(0, 10);
|
||||
return new Date(ms + HALF_DAY_MS).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD → дд.мм.гггг, для текста ошибок валидации. Невалидную строку возвращает как есть. */
|
||||
export function formatIsoDate(iso: string): string {
|
||||
const [y, m, d] = iso.split('-');
|
||||
|
||||
Reference in New Issue
Block a user