feat: подсветка полей, изменённых согласующим, с прежними значениями
Tests / frontend-test (push) Successful in 1m15s
Tests / frontend-test (push) Successful in 1m15s
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { Alert } from '@tnsu/ui-kit';
|
||||
import { IFieldChange } from '../../fieldChanges/fieldChanges';
|
||||
import { formatDateTime } from './formatDateTime';
|
||||
|
||||
interface IFieldChangesBannerProps {
|
||||
changes: IFieldChange[];
|
||||
}
|
||||
|
||||
/** «поле / поля / полей» по числу. */
|
||||
function fieldsWord(count: number): string {
|
||||
const mod10 = count % 10;
|
||||
const mod100 = count % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) return 'поле';
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'поля';
|
||||
return 'полей';
|
||||
}
|
||||
|
||||
/**
|
||||
* Сводка над формой: сколько полей поменял согласующий и когда. Сами поля
|
||||
* подсвечены по месту (см. shared/ui/FormField). На печати не выводится.
|
||||
*/
|
||||
export const FieldChangesBanner: React.FC<IFieldChangesBannerProps> = ({ changes }) => {
|
||||
if (changes.length === 0) return null;
|
||||
|
||||
const names = changes
|
||||
.map((c) => c.byName)
|
||||
.filter((name, i, all) => !!name && all.indexOf(name) === i)
|
||||
.join(', ');
|
||||
const lastAt = changes.reduce((latest, c) => (c.at > latest ? c.at : latest), changes[0].at);
|
||||
|
||||
return (
|
||||
<div className="rp-no-print">
|
||||
<Alert variant="info">
|
||||
Согласующий{names ? ` (${names})` : ''} изменил {changes.length} {fieldsWord(changes.length)}, последнее
|
||||
изменение — {formatDateTime(lastAt)}. Изменённые поля подсвечены, под каждым указано прежнее значение.
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { FieldChangeMap, IFieldChange } from '../../fieldChanges/fieldChanges';
|
||||
|
||||
/**
|
||||
* Журнал правок согласующего для всей формы сразу: каждое поле само находит
|
||||
* свою правку по ключу (см. shared/ui/FormField), поэтому секции форм про
|
||||
* журнал ничего не знают.
|
||||
*/
|
||||
const FieldChangesContext = React.createContext<FieldChangeMap>({});
|
||||
|
||||
interface IFieldChangesProviderProps {
|
||||
changes: FieldChangeMap;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FieldChangesProvider: React.FC<IFieldChangesProviderProps> = ({ changes, children }) => (
|
||||
<FieldChangesContext.Provider value={changes}>{children}</FieldChangesContext.Provider>
|
||||
);
|
||||
|
||||
/** Правка поля по ключу; undefined — поле не менялось (или журнала нет). */
|
||||
export function useFieldChange(fieldKey: string | undefined): IFieldChange | undefined {
|
||||
const changes = React.useContext(FieldChangesContext);
|
||||
return fieldKey ? changes[fieldKey] : undefined;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function pad(n: number): string {
|
||||
return n < 10 ? `0${n}` : String(n);
|
||||
}
|
||||
|
||||
/** ISO-время → «ДД.ММ.ГГГГ ЧЧ:ММ» по локальному времени; битое значение — как есть. */
|
||||
export function formatDateTime(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
// Поле, изменённое согласующим (см. shared/fieldChanges): золотая полоса слева
|
||||
// и лёгкая подложка — заметно, но не спорит с ошибками (красный) и фокусом.
|
||||
.changed {
|
||||
padding: $sp-1 0 $sp-1 $sp-2;
|
||||
border-left: 3px solid $gold-500;
|
||||
border-radius: $r-sm;
|
||||
background: $gold-50;
|
||||
}
|
||||
|
||||
.before {
|
||||
margin-top: $sp-1;
|
||||
font-size: $fs-caption;
|
||||
color: $gray-700;
|
||||
overflow-wrap: anywhere; // длинные значения (списки компаний) не распирают узкую колонку
|
||||
}
|
||||
|
||||
.beforeLabel {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// На печати подсветка не нужна — бумажная форма показывает итоговые значения.
|
||||
@media print {
|
||||
.changed {
|
||||
padding: 0;
|
||||
border-left: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { FormField as KitFormField } from '@tnsu/ui-kit';
|
||||
import styles from './FormField.module.scss';
|
||||
import { useFieldChange } from '../FieldChanges/FieldChangesContext';
|
||||
import { formatDateTime } from '../FieldChanges/formatDateTime';
|
||||
|
||||
interface IFormFieldProps {
|
||||
label: string;
|
||||
htmlFor?: string;
|
||||
required?: boolean;
|
||||
/** Занять обе колонки сетки. */
|
||||
full?: boolean;
|
||||
/**
|
||||
* Ключ поля в журнале правок (internalName колонки). По умолчанию — htmlFor;
|
||||
* задаётся явно у полей без htmlFor (люди, мультивыбор, переключатели).
|
||||
*/
|
||||
fieldKey?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* FormField из ui-kit + подсветка правки согласующего: если поле есть в
|
||||
* журнале (см. FieldChangesProvider), контрол подсвечивается, а под ним —
|
||||
* прежнее значение. Без журнала ведёт себя ровно как FormField из ui-kit.
|
||||
*/
|
||||
export const FormField: React.FC<IFormFieldProps> = ({ fieldKey, children, ...fieldProps }) => {
|
||||
const change = useFieldChange(fieldKey ?? fieldProps.htmlFor);
|
||||
if (!change) {
|
||||
return <KitFormField {...fieldProps}>{children}</KitFormField>;
|
||||
}
|
||||
|
||||
return (
|
||||
<KitFormField {...fieldProps}>
|
||||
<div className={styles.changed}>{children}</div>
|
||||
<div className={`${styles.before} rp-no-print`} title={`Изменено: ${change.byName}, ${formatDateTime(change.at)}`}>
|
||||
<span className={styles.beforeLabel}>Было:</span> {change.before}
|
||||
</div>
|
||||
</KitFormField>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,6 @@ export {
|
||||
Alert,
|
||||
FormCard,
|
||||
FormGrid,
|
||||
FormField,
|
||||
TextInput,
|
||||
PhoneInput,
|
||||
MoneyInput,
|
||||
@@ -32,6 +31,11 @@ export {
|
||||
export type { ISelectOption, IConfirmOptions } from '@tnsu/ui-kit';
|
||||
|
||||
// --- Пока локальные (не перенесены: SP/бизнес/отложенные) ---
|
||||
// FormField — обёртка над ui-kit с подсветкой правок согласующего (журнал в
|
||||
// FieldChangesProvider); без журнала ведёт себя как FormField из ui-kit.
|
||||
export { FormField } from './FormField/FormField';
|
||||
export { FieldChangesProvider } from './FieldChanges/FieldChangesContext';
|
||||
export { FieldChangesBanner } from './FieldChanges/FieldChangesBanner';
|
||||
export { ItemsTable } from './ItemsTable/ItemsTable';
|
||||
export type { IItemsTableProps } from './ItemsTable/ItemsTable';
|
||||
export type { ILineItem } from './ItemsTable/ILineItem';
|
||||
|
||||
Reference in New Issue
Block a user