Слияние main в feature/vacation-edo-signing: исправление дат «только дата» в отпуске без содержания поверх перехода на «начало + количество дней»
Tests / frontend-test (pull_request) Successful in 1m25s
Tests / frontend-test (pull_request) Successful in 1m25s
This commit is contained in:
@@ -480,7 +480,8 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
sourceId: route.sourceId,
|
sourceId: route.sourceId,
|
||||||
positionId: route.positionId,
|
positionId: route.positionId,
|
||||||
nomenclatureId: route.nomenclatureId,
|
nomenclatureId: route.nomenclatureId,
|
||||||
storeName: route.storeName
|
storeName: route.storeName,
|
||||||
|
sourceSystem: route.sourceSystem
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ export interface ISubmitFeatureProps {
|
|||||||
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
|
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
|
||||||
* sourceId задан) или без него (пустая новая форма конкретного типа).
|
* sourceId задан) или без него (пустая новая форма конкретного типа).
|
||||||
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
|
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
|
||||||
initialForm?: { formKey: string; sourceId?: number; positionId?: string; nomenclatureId?: string; storeName?: string };
|
initialForm?: {
|
||||||
|
formKey: string;
|
||||||
|
sourceId?: number;
|
||||||
|
positionId?: string;
|
||||||
|
nomenclatureId?: string;
|
||||||
|
storeName?: string;
|
||||||
|
sourceSystem?: string;
|
||||||
|
};
|
||||||
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
|
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
|
||||||
editForm?: { formKey: string; itemId: number };
|
editForm?: { formKey: string; itemId: number };
|
||||||
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
|
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
|
||||||
@@ -145,6 +152,7 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
positionId={external?.positionId}
|
positionId={external?.positionId}
|
||||||
nomenclatureId={external?.nomenclatureId}
|
nomenclatureId={external?.nomenclatureId}
|
||||||
storeName={external?.storeName}
|
storeName={external?.storeName}
|
||||||
|
sourceSystem={external?.sourceSystem}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
import { parseTasks } from './tripData';
|
import { parseTasks } from './tripData';
|
||||||
import { parseRoute } from './route';
|
import { parseRoute } from './route';
|
||||||
@@ -42,8 +43,7 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string):
|
|||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
// Колонка «только дата» — время в ответе нулевое, берём дату как есть.
|
SubmitDate: spDateOnlyToIso(item.SubmitDate),
|
||||||
SubmitDate: typeof item.SubmitDate === 'string' ? item.SubmitDate.slice(0, 10) : '',
|
|
||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
Position: (item.Position as string) || '',
|
Position: (item.Position as string) || '',
|
||||||
Organization: (item.Organization as string) || '',
|
Organization: (item.Organization as string) || '',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { effectiveUserEmail } from '../../shared/impersonation';
|
|||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
|
import { todayIsoDate } from '../../shared/dateUtils';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
approvalListTitleForListId,
|
approvalListTitleForListId,
|
||||||
@@ -111,7 +112,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
|
|||||||
if (editId) {
|
if (editId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = todayIsoDate();
|
||||||
setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today }));
|
setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today }));
|
||||||
}, [editId]);
|
}, [editId]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
/** Число или '' (SharePoint отдаёт null для незаполненного Number). */
|
/** Число или '' (SharePoint отдаёт null для незаполненного Number). */
|
||||||
@@ -60,7 +61,7 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
|
|||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
Position: (item.Position as string) || '',
|
Position: (item.Position as string) || '',
|
||||||
Department: (item.Department 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,
|
ParkingCard: !!item.ParkingCard,
|
||||||
AccessCard: !!item.AccessCard,
|
AccessCard: !!item.AccessCard,
|
||||||
// Хранится в WriteOffTrainingAmount, не в одноимённом TrainingAmount — см.
|
// Хранится в WriteOffTrainingAmount, не в одноимённом TrainingAmount — см.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, IConcreteItem, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, IConcreteItem, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -53,7 +54,7 @@ export async function loadConcreteRequest(itemId: number, listId?: string): Prom
|
|||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
RequestDate: requestDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
RequestDate: spDateOnlyToIso(requestDate),
|
||||||
SiteChiefEmail: (item.SiteChiefEmail as string) || '',
|
SiteChiefEmail: (item.SiteChiefEmail as string) || '',
|
||||||
SiteChiefName: (item.SiteChiefName as string) || '',
|
SiteChiefName: (item.SiteChiefName as string) || '',
|
||||||
ForemanEmail: (item.ForemanEmail as string) || '',
|
ForemanEmail: (item.ForemanEmail as string) || '',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { requestLinkUrl } from '../../shared/routing';
|
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 { FORM_ID as CLEARANCE_FORM_ID, EMPTY_DATA as CLEARANCE_EMPTY, IFormData as IClearanceData } from '../Clearance/model';
|
||||||
import { clearanceApprovalService, saveClearanceRequest } from '../Clearance/submit';
|
import { clearanceApprovalService, saveClearanceRequest } from '../Clearance/submit';
|
||||||
import { FORM_ID, primaryPosition, zipOrganization } from './model';
|
import { FORM_ID, primaryPosition, zipOrganization } from './model';
|
||||||
@@ -136,7 +137,7 @@ export async function ensureClearanceForDismissal(
|
|||||||
Position: position,
|
Position: position,
|
||||||
Department: department,
|
Department: department,
|
||||||
// Последний рабочий день заявления = дата расторжения договора в обходном листе.
|
// Последний рабочий день заявления = дата расторжения договора в обходном листе.
|
||||||
TerminationDate: ((item.LastWorkingDay as string) || '').slice(0, 10),
|
TerminationDate: spDateOnlyToIso(item.LastWorkingDay),
|
||||||
Organization: organizationText
|
Organization: organizationText
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, zipOrganization } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, zipOrganization } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -47,7 +48,7 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro
|
|||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
LastWorkingDay: lastDay.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
LastWorkingDay: spDateOnlyToIso(lastDay),
|
||||||
Phone: (item.Phone as string) || '',
|
Phone: (item.Phone as string) || '',
|
||||||
Comment: (item.Comment as string) || '',
|
Comment: (item.Comment as string) || '',
|
||||||
Organization: zipOrganization(item.Organization, item.Department, item.Position),
|
Organization: zipOrganization(item.Organization, item.Department, item.Position),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -47,7 +48,7 @@ export async function loadEmployeeTransferRequest(itemId: number, listId?: strin
|
|||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
Position: (item.Position as string) || '',
|
Position: (item.Position as string) || '',
|
||||||
Organization: (item.Organization 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) || '',
|
NewPosition: (item.NewPosition as string) || '',
|
||||||
NewDepartment: (item.NewDepartment as string) || '',
|
NewDepartment: (item.NewDepartment as string) || '',
|
||||||
NewDepartmentId: (item.NewDepartmentId as string) || '',
|
NewDepartmentId: (item.NewDepartmentId as string) || '',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, ICompanyShare, LEGACY_HIRE_TYPES } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, ICompanyShare, LEGACY_HIRE_TYPES } from './model';
|
||||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||||
@@ -76,7 +77,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
|||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
writable[key] = '';
|
writable[key] = '';
|
||||||
} else if (key === 'HireHireDate' && typeof value === 'string') {
|
} 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 {
|
} else {
|
||||||
writable[key] = String(value);
|
writable[key] = String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, ILanguageLevel } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, ILanguageLevel } from './model';
|
||||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||||
@@ -103,7 +104,7 @@ export async function loadSelectionRequest(itemId: number, listId?: string): Pro
|
|||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
writable[key] = '';
|
writable[key] = '';
|
||||||
} else if (DATE_FIELDS.indexOf(key) !== -1 && typeof value === 'string') {
|
} 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 {
|
} else {
|
||||||
writable[key] = String(value);
|
writable[key] = String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
||||||
@@ -72,7 +73,7 @@ export async function loadReferralRequest(itemId: number, listId?: string): Prom
|
|||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
SubmitDate: submitDate.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
SubmitDate: spDateOnlyToIso(submitDate),
|
||||||
Referrer: referrer?.EMail || '',
|
Referrer: referrer?.EMail || '',
|
||||||
ReferrerPosition: (item.ReferrerPosition as string) || '',
|
ReferrerPosition: (item.ReferrerPosition as string) || '',
|
||||||
ReferrerDepartment: (item.ReferrerDepartment as string) || '',
|
ReferrerDepartment: (item.ReferrerDepartment as string) || '',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
|
import { todayIsoDate } from '../../shared/dateUtils';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
@@ -88,7 +89,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = context.pageContext.user;
|
const user = context.pageContext.user;
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = todayIsoDate();
|
||||||
const requesterEmail = effectiveUserEmail(context);
|
const requesterEmail = effectiveUserEmail(context);
|
||||||
setData((prev) => ({
|
setData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { parseGroupedJson } from './cost';
|
import { parseGroupedJson } from './cost';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, EMPTY_ANNEX_ROW, IAnnexRow } from './model';
|
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. */
|
/** ISO-дата из SharePoint → YYYY-MM-DD для input type=date. */
|
||||||
function toDateInput(raw: unknown): string {
|
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 { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
import { parseMissedDays } from './missedDaysJson';
|
import { parseMissedDays } from './missedDaysJson';
|
||||||
import { toDateKey } from './skud/skudDays';
|
import { toDateKey } from './skud/skudDays';
|
||||||
@@ -52,8 +53,7 @@ export async function loadSkudExplanationRequest(itemId: number, listId?: string
|
|||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
// Колонка «только дата» — время в ответе нулевое, берём дату как есть (как в Referral/load.ts).
|
SubmitDate: spDateOnlyToIso(item.SubmitDate),
|
||||||
SubmitDate: ((item.SubmitDate as string) || '').slice(0, 10),
|
|
||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
EmployeeFio: (item.SkudEmployeeFio as string) || employee?.Title || '',
|
EmployeeFio: (item.SkudEmployeeFio as string) || employee?.Title || '',
|
||||||
Position: (item.Position as string) || '',
|
Position: (item.Position as string) || '',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SPHttpClient } from '@microsoft/sp-http';
|
import { SPHttpClient } from '@microsoft/sp-http';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { HOLIDAYS_LIST_TITLE, HOLIDAYS_SITE_URL } from '../../../shared/config';
|
import { HOLIDAYS_LIST_TITLE, HOLIDAYS_SITE_URL } from '../../../shared/config';
|
||||||
|
import { spDateOnlyToIso } from '../../../shared/dateUtils';
|
||||||
|
|
||||||
interface IHolidayItem {
|
interface IHolidayItem {
|
||||||
Title?: string;
|
Title?: string;
|
||||||
@@ -25,7 +26,7 @@ async function fetchHolidays(context: WebPartContext): Promise<Map<string, strin
|
|||||||
const body = (await response.json()) as { value?: IHolidayItem[] };
|
const body = (await response.json()) as { value?: IHolidayItem[] };
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
(body.value || []).forEach((item) => {
|
(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;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { requestLinkUrl } from '../../shared/routing';
|
|||||||
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
||||||
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
|
import { todayIsoDate } from '../../shared/dateUtils';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
@@ -93,7 +94,8 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
onCodeLoaded,
|
onCodeLoaded,
|
||||||
positionId,
|
positionId,
|
||||||
nomenclatureId,
|
nomenclatureId,
|
||||||
storeName
|
storeName,
|
||||||
|
sourceSystem
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
@@ -121,7 +123,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = context.pageContext.user;
|
const user = context.pageContext.user;
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = todayIsoDate();
|
||||||
const requesterEmail = effectiveUserEmail(context);
|
const requesterEmail = effectiveUserEmail(context);
|
||||||
setData((prev) => ({
|
setData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -146,18 +148,30 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
};
|
};
|
||||||
}, [editId, context]);
|
}, [editId, context]);
|
||||||
|
|
||||||
// Открытие по внешней ссылке (positionId + nomenclatureId в URL): запоминаем
|
// Открытие по внешней ссылке: запоминаем, откуда пришла заявка — система
|
||||||
// строку-источник и предзаполняем таблицу остатками номенклатуры по складам.
|
// (source), строка-источник (positionId) и склад. Строка нужна при отправке для
|
||||||
// Только для новой заявки — при редактировании позиции уже сохранены.
|
// обратной записи ссылки в Dataverse, система — чтобы знать, откуда заявка,
|
||||||
|
// склад — справочно (см. model.ts). Не зависит от nomenclatureId: ссылка может
|
||||||
|
// прийти и без товара. Только для новой заявки — у сохранённой всё уже в SP.
|
||||||
|
useEffect(() => {
|
||||||
|
if (editId || (!positionId && !storeName && !sourceSystem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
SourcePositionId: positionId || '',
|
||||||
|
SourceStoreName: storeName || '',
|
||||||
|
SourceSystem: sourceSystem || ''
|
||||||
|
}));
|
||||||
|
}, [editId, positionId, storeName, sourceSystem]);
|
||||||
|
|
||||||
|
// Предзаполнение таблицы остатками номенклатуры по складам (nomenclatureId из
|
||||||
|
// внешней ссылки). nomenclatureId отдельно в шапке не храним — он едет в каждой
|
||||||
|
// позиции ItemsJson. Только для новой заявки — при редактировании позиции уже сохранены.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editId || !nomenclatureId) {
|
if (editId || !nomenclatureId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Строку-источник и склад фиксируем сразу, независимо от ответа справочника:
|
|
||||||
// строка нужна при отправке для обратной записи ссылки в Dataverse, склад —
|
|
||||||
// только справочно для пользователя (см. SourceStoreName в model.ts).
|
|
||||||
// nomenclatureId отдельно в шапке не храним — он едет в каждой позиции ItemsJson.
|
|
||||||
setData((prev) => ({ ...prev, SourcePositionId: positionId || '', SourceStoreName: storeName || '' }));
|
|
||||||
let alive = true;
|
let alive = true;
|
||||||
getNomenclature(context, nomenclatureId, storeName)
|
getNomenclature(context, nomenclatureId, storeName)
|
||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
@@ -177,7 +191,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [editId, nomenclatureId, positionId, storeName, context, setSubmitError]);
|
}, [editId, nomenclatureId, storeName, context, setSubmitError]);
|
||||||
|
|
||||||
// Проект-получатель («Куда») по той же внешней ссылке: строка-источник в
|
// Проект-получатель («Куда») по той же внешней ссылке: строка-источник в
|
||||||
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -53,6 +54,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
'ContactPhone',
|
'ContactPhone',
|
||||||
'SourcePositionId',
|
'SourcePositionId',
|
||||||
'SourceStoreName',
|
'SourceStoreName',
|
||||||
|
'SourceSystem',
|
||||||
'ItemsJson',
|
'ItemsJson',
|
||||||
'RequestStatus',
|
'RequestStatus',
|
||||||
'RequestUniqueCode'
|
'RequestUniqueCode'
|
||||||
@@ -69,7 +71,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
Department: (item.Department as string) || '',
|
Department: (item.Department as string) || '',
|
||||||
DepartmentId: (item.DepartmentId as string) || '',
|
DepartmentId: (item.DepartmentId as string) || '',
|
||||||
Subject: (item.Subject 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) || '',
|
SourceProject: (item.SourceProject as string) || '',
|
||||||
SourceProjectId: (item.SourceProjectId as string) || '',
|
SourceProjectId: (item.SourceProjectId as string) || '',
|
||||||
SourceProjectManager: (item.SourceProjectManager as string) || '',
|
SourceProjectManager: (item.SourceProjectManager as string) || '',
|
||||||
@@ -80,6 +82,7 @@ export async function loadTmcTransferRequest(itemId: number, listId?: string): P
|
|||||||
ContactPhone: (item.ContactPhone as string) || '',
|
ContactPhone: (item.ContactPhone as string) || '',
|
||||||
SourcePositionId: (item.SourcePositionId as string) || '',
|
SourcePositionId: (item.SourcePositionId as string) || '',
|
||||||
SourceStoreName: (item.SourceStoreName as string) || '',
|
SourceStoreName: (item.SourceStoreName as string) || '',
|
||||||
|
SourceSystem: (item.SourceSystem as string) || '',
|
||||||
Items: parseItems(item.ItemsJson)
|
Items: parseItems(item.ItemsJson)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ export interface IFormData {
|
|||||||
* именно списываются ТМЦ. '' — ссылка без фильтра по складу или заявка заведена вручную.
|
* именно списываются ТМЦ. '' — ссылка без фильтра по складу или заявка заведена вручную.
|
||||||
*/
|
*/
|
||||||
SourceStoreName: string;
|
SourceStoreName: string;
|
||||||
|
/**
|
||||||
|
* Система, из которой открыли форму по внешней ссылке — параметр `source` в URL
|
||||||
|
* (напр. 'kps'). Вместе с SourcePositionId однозначно указывает, откуда пришла
|
||||||
|
* заявка и куда отвечать. '' — заявка заведена вручную или ссылка без source.
|
||||||
|
*/
|
||||||
|
SourceSystem: string;
|
||||||
|
|
||||||
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
|
// Табличная часть — позиции ТМЦ (сериализуется в ItemsJson). У строк из
|
||||||
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
|
// справочника есть nomenclatureId (см. ILineItem), у дописанных вручную — нет.
|
||||||
@@ -115,5 +121,6 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
ContactPhone: '',
|
ContactPhone: '',
|
||||||
SourcePositionId: '',
|
SourcePositionId: '',
|
||||||
SourceStoreName: '',
|
SourceStoreName: '',
|
||||||
|
SourceSystem: '',
|
||||||
Items: []
|
Items: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
|
{ "internalName": "SourcePositionId", "title": "Id строки-источника (Dataverse)", "type": "Text" },
|
||||||
{ "internalName": "SourceStoreName", "title": "Склад-источник (внешняя ссылка)", "type": "Text" },
|
{ "internalName": "SourceStoreName", "title": "Склад-источник (внешняя ссылка)", "type": "Text" },
|
||||||
|
{ "internalName": "SourceSystem", "title": "Система-источник (внешняя ссылка)", "type": "Text" },
|
||||||
|
|
||||||
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
{ "internalName": "ItemsJson", "title": "Позиции ТМЦ (JSON)", "type": "Note" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -51,7 +52,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
|||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
Position: (item.Position as string) || '',
|
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),
|
DaysCount: days === null || days === undefined ? '' : String(days),
|
||||||
Reason: (item.Reason as string) || '',
|
Reason: (item.Reason as string) || '',
|
||||||
Substitute: substitute?.EMail || '',
|
Substitute: substitute?.EMail || '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
@@ -50,7 +51,7 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
|
|||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
Employee: employee?.EMail || '',
|
Employee: employee?.EMail || '',
|
||||||
Position: (item.Position as string) || '',
|
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),
|
DaysCount: days === null || days === undefined ? '' : String(days),
|
||||||
Substitute: substitute?.EMail || '',
|
Substitute: substitute?.EMail || '',
|
||||||
Organization: (item.Organization as string) || '',
|
Organization: (item.Organization as string) || '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { dataListTitle } from '../../shared/listTitles';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, EMPTY_DATA } from './model';
|
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) {
|
if (value === null || value === undefined) {
|
||||||
writable[key] = '';
|
writable[key] = '';
|
||||||
} else if (key === 'StartDate' && typeof value === 'string') {
|
} else if (key === 'StartDate' && typeof value === 'string') {
|
||||||
writable[key] = value.slice(0, 10);
|
writable[key] = spDateOnlyToIso(value);
|
||||||
} else {
|
} else {
|
||||||
writable[key] = String(value);
|
writable[key] = String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export interface IRequestFormProps {
|
|||||||
nomenclatureId?: string;
|
nomenclatureId?: string;
|
||||||
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
|
/** Склад-фильтр для справочника номенклатуры (см. positionId). */
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
|
/** Система-источник внешней ссылки (параметр `source`, напр. 'kps'; см. positionId). */
|
||||||
|
sourceSystem?: string;
|
||||||
/**
|
/**
|
||||||
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
* Id СВОЕЙ заявки, открытой на редактирование (черновик / возвращённая /
|
||||||
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
|
* отклонённая). В отличие от sourceId это не предзаполнение новой заявки, а
|
||||||
|
|||||||
@@ -159,11 +159,15 @@ export const DATAVERSE_API_VERSION = 'v9.2';
|
|||||||
*/
|
*/
|
||||||
export const DATAVERSE_DEMANDLINE_ENTITY_SET = 'cree9_demandlines';
|
export const DATAVERSE_DEMANDLINE_ENTITY_SET = 'cree9_demandlines';
|
||||||
/**
|
/**
|
||||||
* Колонка-URL, куда пишем ссылку на заявку портала (создана вручную в Dataverse).
|
* Колонка, куда пишем ссылку на заявку портала (создана вручную в Dataverse,
|
||||||
* NB: префикс cr498_ (издатель среды org95ca17d6.crm4 по умолчанию), а НЕ cree9_
|
* отображаемое имя «cree9_tmctransferurl», однострочный текст). Указана владельцами
|
||||||
* как у остальных колонок таблицы — так сгенерировалось при создании колонки.
|
* внешней системы; прежняя URL-колонка cr498_portalrequesturl больше не используется.
|
||||||
|
* NB: логическое имя с префиксом cr498_ (издатель среды org95ca17d6.crm4 по
|
||||||
|
* умолчанию) — так сгенерировалось при создании колонки.
|
||||||
|
* Ссылка на заявку ~200+ символов (кириллица в пути страницы кодируется), поэтому
|
||||||
|
* макс. длина колонки должна быть не меньше 500 — иначе PATCH отклонится.
|
||||||
*/
|
*/
|
||||||
export const DATAVERSE_DEMANDLINE_LINK_FIELD = 'cr498_portalrequesturl';
|
export const DATAVERSE_DEMANDLINE_LINK_FIELD = 'cr498_cree9_tmctransferurl';
|
||||||
/**
|
/**
|
||||||
* Колонка «ID позиции ЛЗК» на «Строке заявки» — GUID строки cree9_lzkposition
|
* Колонка «ID позиции ЛЗК» на «Строке заявки» — GUID строки cree9_lzkposition
|
||||||
* ХРАНИТСЯ ТЕКСТОМ (не lookup-связью), поэтому её нельзя $expand'нуть — только
|
* ХРАНИТСЯ ТЕКСТОМ (не lookup-связью), поэтому её нельзя $expand'нуть — только
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
*
|
*
|
||||||
* positionId — GUID первичного ключа строки (приходит в URL как positionId, см.
|
* positionId — GUID первичного ключа строки (приходит в URL как positionId, см.
|
||||||
* shared/routing). Адресуем строку напрямую: <entitySet>(<guid>). PATCH пишет
|
* shared/routing). Адресуем строку напрямую: <entitySet>(<guid>). PATCH пишет
|
||||||
* URL в колонку cree9_portalrequesturl.
|
* URL в колонку DATAVERSE_DEMANDLINE_LINK_FIELD (см. shared/config).
|
||||||
*
|
*
|
||||||
* Заголовок If-Match: * ОБЯЗАТЕЛЕН: без него PATCH в Dataverse — это upsert (нет
|
* Заголовок If-Match: * ОБЯЗАТЕЛЕН: без него PATCH в Dataverse — это upsert (нет
|
||||||
* строки с таким id → СОЗДАЁТ новую). Нам нужно строго обновление существующей
|
* строки с таким id → СОЗДАЁТ новую). Нам нужно строго обновление существующей
|
||||||
|
|||||||
@@ -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())}`;
|
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 → дд.мм.гггг, для текста ошибок валидации. Невалидную строку возвращает как есть. */
|
/** YYYY-MM-DD → дд.мм.гггг, для текста ошибок валидации. Невалидную строку возвращает как есть. */
|
||||||
export function formatIsoDate(iso: string): string {
|
export function formatIsoDate(iso: string): string {
|
||||||
const [y, m, d] = iso.split('-');
|
const [y, m, d] = iso.split('-');
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ describe('parseRoute', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('внешняя ссылка ТМЦ: source прокидывается в create как sourceSystem', () => {
|
||||||
|
expect(parseRoute('?rptype=wh-tmc-transfer&positionId=abc&source=kps').sourceSystem).toBe('kps');
|
||||||
|
});
|
||||||
|
|
||||||
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
|
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
|
||||||
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
||||||
// себе не должны влиять на маршрут.
|
// себе не должны влиять на маршрут.
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export interface IRoute {
|
|||||||
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
|
* этому складу. Приходит по той же внешней ссылке; пусто — все склады.
|
||||||
*/
|
*/
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
|
/**
|
||||||
|
* Система, из которой открыли форму по внешней ссылке (параметр `source`,
|
||||||
|
* напр. 'kps'). Сохраняется в заявке, чтобы знать, откуда она пришла и куда
|
||||||
|
* отвечать. Не путать с sourceId (rpsource) — тот про предзаполнение из
|
||||||
|
* другой заявки портала.
|
||||||
|
*/
|
||||||
|
sourceSystem?: string;
|
||||||
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
||||||
ownTab?: OwnTab;
|
ownTab?: OwnTab;
|
||||||
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
||||||
@@ -378,6 +385,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
const positionId = params.get('positionId') ?? undefined;
|
const positionId = params.get('positionId') ?? undefined;
|
||||||
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
|
const nomenclatureId = params.get('nomenclatureId') ?? undefined;
|
||||||
const storeName = params.get('storeName') ?? undefined;
|
const storeName = params.get('storeName') ?? undefined;
|
||||||
|
const sourceSystem = params.get('source') ?? undefined;
|
||||||
|
|
||||||
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
||||||
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
||||||
@@ -432,7 +440,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
|
// rpmode=approve без валидного rpid) должна откатываться на обычную подачу,
|
||||||
// а не тихо превращаться в открытие пустой формы другого режима.
|
// а не тихо превращаться в открытие пустой формы другого режима.
|
||||||
if (formKey && mode !== 'approve' && mode !== 'edit') {
|
if (formKey && mode !== 'approve' && mode !== 'edit') {
|
||||||
return { mode: 'create', nav: 'new', formKey, positionId, nomenclatureId, storeName };
|
return { mode: 'create', nav: 'new', formKey, positionId, nomenclatureId, storeName, sourceSystem };
|
||||||
}
|
}
|
||||||
return { mode: 'submit', nav, ownTab, ownStatus, allTypeId, page, query };
|
return { mode: 'submit', nav, ownTab, ownStatus, allTypeId, page, query };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user