diff --git a/src/webparts/requestPortal/forms/BusinessTrip/load.ts b/src/webparts/requestPortal/forms/BusinessTrip/load.ts index 426950b..a0c1d76 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/load.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index 0b22ae0..d339b50 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -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 = ({ if (editId) { return; } - const today = new Date().toISOString().slice(0, 10); + const today = todayIsoDate(); setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today })); }, [editId]); diff --git a/src/webparts/requestPortal/forms/Clearance/load.ts b/src/webparts/requestPortal/forms/Clearance/load.ts index 51d283c..6ae3597 100644 --- a/src/webparts/requestPortal/forms/Clearance/load.ts +++ b/src/webparts/requestPortal/forms/Clearance/load.ts @@ -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 — см. diff --git a/src/webparts/requestPortal/forms/ConcreteRequest/load.ts b/src/webparts/requestPortal/forms/ConcreteRequest/load.ts index e76f527..8ed2cdb 100644 --- a/src/webparts/requestPortal/forms/ConcreteRequest/load.ts +++ b/src/webparts/requestPortal/forms/ConcreteRequest/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/Dismissal/createClearance.ts b/src/webparts/requestPortal/forms/Dismissal/createClearance.ts index 421655a..a155070 100644 --- a/src/webparts/requestPortal/forms/Dismissal/createClearance.ts +++ b/src/webparts/requestPortal/forms/Dismissal/createClearance.ts @@ -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 }; diff --git a/src/webparts/requestPortal/forms/Dismissal/load.ts b/src/webparts/requestPortal/forms/Dismissal/load.ts index b066ea1..a3ca682 100644 --- a/src/webparts/requestPortal/forms/Dismissal/load.ts +++ b/src/webparts/requestPortal/forms/Dismissal/load.ts @@ -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), diff --git a/src/webparts/requestPortal/forms/EmployeeTransfer/load.ts b/src/webparts/requestPortal/forms/EmployeeTransfer/load.ts index c897b9d..39564f2 100644 --- a/src/webparts/requestPortal/forms/EmployeeTransfer/load.ts +++ b/src/webparts/requestPortal/forms/EmployeeTransfer/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index f645ab3..954351f 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -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); } diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/load.ts b/src/webparts/requestPortal/forms/RecruitingSelection/load.ts index 27a2848..ee0e9da 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/load.ts @@ -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); } diff --git a/src/webparts/requestPortal/forms/Referral/load.ts b/src/webparts/requestPortal/forms/Referral/load.ts index 48a49e0..f67d797 100644 --- a/src/webparts/requestPortal/forms/Referral/load.ts +++ b/src/webparts/requestPortal/forms/Referral/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestForm.tsx b/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestForm.tsx index 0c2354f..7cd9029 100644 --- a/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestForm.tsx +++ b/src/webparts/requestPortal/forms/ServiceRequest/ServiceRequestForm.tsx @@ -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 = ({ return; } const user = context.pageContext.user; - const today = new Date().toISOString().slice(0, 10); + const today = todayIsoDate(); const requesterEmail = effectiveUserEmail(context); setData((prev) => ({ ...prev, diff --git a/src/webparts/requestPortal/forms/ServiceRequest/load.ts b/src/webparts/requestPortal/forms/ServiceRequest/load.ts index de097e2..b2959df 100644 --- a/src/webparts/requestPortal/forms/ServiceRequest/load.ts +++ b/src/webparts/requestPortal/forms/ServiceRequest/load.ts @@ -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(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); } /** diff --git a/src/webparts/requestPortal/forms/SkudExplanation/load.ts b/src/webparts/requestPortal/forms/SkudExplanation/load.ts index 5491db0..92d8736 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/load.ts +++ b/src/webparts/requestPortal/forms/SkudExplanation/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/SkudExplanation/skud/holidays.service.ts b/src/webparts/requestPortal/forms/SkudExplanation/skud/holidays.service.ts index c965d1e..f3aba16 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/skud/holidays.service.ts +++ b/src/webparts/requestPortal/forms/SkudExplanation/skud/holidays.service.ts @@ -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(); (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; } diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index 32a664c..071562f 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -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 = ({ return; } const user = context.pageContext.user; - const today = new Date().toISOString().slice(0, 10); + const today = todayIsoDate(); const requesterEmail = effectiveUserEmail(context); setData((prev) => ({ ...prev, diff --git a/src/webparts/requestPortal/forms/TmcTransfer/load.ts b/src/webparts/requestPortal/forms/TmcTransfer/load.ts index 47969d4..05c59a8 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/load.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/load.ts b/src/webparts/requestPortal/forms/UnpaidLeave/load.ts index 25ace41..4d2867b 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/load.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/Vacation/load.ts b/src/webparts/requestPortal/forms/Vacation/load.ts index 5bd8218..34d5c16 100644 --- a/src/webparts/requestPortal/forms/Vacation/load.ts +++ b/src/webparts/requestPortal/forms/Vacation/load.ts @@ -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) || '', diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts index b04d12a..3170777 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts @@ -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); } diff --git a/src/webparts/requestPortal/shared/dateUtils.test.ts b/src/webparts/requestPortal/shared/dateUtils.test.ts new file mode 100644 index 0000000..8711669 --- /dev/null +++ b/src/webparts/requestPortal/shared/dateUtils.test.ts @@ -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); +} diff --git a/src/webparts/requestPortal/shared/dateUtils.ts b/src/webparts/requestPortal/shared/dateUtils.ts index 88a4d1b..1094531 100644 --- a/src/webparts/requestPortal/shared/dateUtils.ts +++ b/src/webparts/requestPortal/shared/dateUtils.ts @@ -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('-');