fix(1539): единицы измерения в сводной ОСВ, полоса прокрутки
This commit is contained in:
parent
723c84cc72
commit
71ef6333b5
@ -1,6 +1,6 @@
|
|||||||
import { DownloadOutlined, FilterOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, FilterOutlined } from '@ant-design/icons';
|
||||||
import { Alert, Button, Space, Tooltip, message } from 'antd';
|
import { Alert, Button, InputNumber, Select, Space, Tooltip, message } from 'antd';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { ApiError } from '../api/client';
|
import { ApiError } from '../api/client';
|
||||||
import PageTitle from '../components/PageTitle';
|
import PageTitle from '../components/PageTitle';
|
||||||
@ -13,7 +13,7 @@ import type { ConsRussianStatus } from '../types/consolidations/common_types';
|
|||||||
import type { VersionCard } from '../types/consolidations/version_members';
|
import type { VersionCard } from '../types/consolidations/version_members';
|
||||||
import { exportFilename, saveBlob } from '../utils/download';
|
import { exportFilename, saveBlob } from '../utils/download';
|
||||||
import { quarterShortLabel } from '../utils/labels';
|
import { quarterShortLabel } from '../utils/labels';
|
||||||
import { statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts';
|
import { consStatusDescriptionMapping, statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts';
|
||||||
import { CounterpartyMapModal } from '../widgets/ConsolidatedOsv/CounterpartyMapModal';
|
import { CounterpartyMapModal } from '../widgets/ConsolidatedOsv/CounterpartyMapModal';
|
||||||
import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps';
|
import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps';
|
||||||
import { OsvTable } from '../widgets/ConsolidatedOsv/OsvTable';
|
import { OsvTable } from '../widgets/ConsolidatedOsv/OsvTable';
|
||||||
@ -22,6 +22,7 @@ import { mapEntryId } from '../widgets/ConsolidatedOsv/counterpartyMapping';
|
|||||||
import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../widgets/ConsolidatedOsv/useConsolidatedOsv';
|
import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../widgets/ConsolidatedOsv/useConsolidatedOsv';
|
||||||
import { useCurrentUser } from '../auth/MeContext';
|
import { useCurrentUser } from '../auth/MeContext';
|
||||||
import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal';
|
import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal';
|
||||||
|
import { UNITS, useNumberDisplay } from '../widgets/ConsolidatedOsv/useNumberDisplay';
|
||||||
|
|
||||||
function OsvContent({ versionId }: { versionId: string }) {
|
function OsvContent({ versionId }: { versionId: string }) {
|
||||||
const [card, setCard] = useState<VersionCard | null>(null);
|
const [card, setCard] = useState<VersionCard | null>(null);
|
||||||
@ -31,6 +32,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
|||||||
const [mapping, setMapping] = useState<ConsolidatedRowOut | null>(null);
|
const [mapping, setMapping] = useState<ConsolidatedRowOut | null>(null);
|
||||||
const osv = useConsolidatedOsv(versionId);
|
const osv = useConsolidatedOsv(versionId);
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
|
const { unit, setUnit, precision, setPrecision, format } = useNumberDisplay();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let actual = true;
|
let actual = true;
|
||||||
@ -62,6 +64,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
|||||||
const unmapped = s?.unmapped_total ?? 0;
|
const unmapped = s?.unmapped_total ?? 0;
|
||||||
const onlyUnmapped = osv.filters.unmapped === true;
|
const onlyUnmapped = osv.filters.unmapped === true;
|
||||||
const russianStatus = card ? (statusMapping[card.status] ?? card.status) : '';
|
const russianStatus = card ? (statusMapping[card.status] ?? card.status) : '';
|
||||||
|
const description = card ? (consStatusDescriptionMapping[card.status] ?? russianStatus) : '';
|
||||||
const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default;
|
const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default;
|
||||||
const groupName = card?.company_group.name ?? '';
|
const groupName = card?.company_group.name ?? '';
|
||||||
const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : '';
|
const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : '';
|
||||||
@ -71,7 +74,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
|||||||
backable
|
backable
|
||||||
location={['Отчётность', groupName, period]}
|
location={['Отчётность', groupName, period]}
|
||||||
header={card ? `${groupName} — ${card.year} ${quarterShortLabel(card.quarter)}` : 'Сводная ОСВ'}
|
header={card ? `${groupName} — ${card.year} ${quarterShortLabel(card.quarter)}` : 'Сводная ОСВ'}
|
||||||
description={russianStatus}
|
description={description}
|
||||||
extra={
|
extra={
|
||||||
card && (
|
card && (
|
||||||
<Space size={8} style={{ display: 'flex', gap: 4 }}>
|
<Space size={8} style={{ display: 'flex', gap: 4 }}>
|
||||||
@ -121,7 +124,23 @@ function OsvContent({ versionId }: { versionId: string }) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 10, color: '#667085' }}>Единицы</div>
|
||||||
|
<Select variant='underlined' value={unit} onChange={setUnit} options={UNITS} style={{ width: 140 }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 10, color: '#667085' }}>Знаков после запятой</div>
|
||||||
|
<Select
|
||||||
|
variant='underlined'
|
||||||
|
options={[0, 1, 2, 3].map((i) => ({ label: i, value: i }))}
|
||||||
|
value={precision}
|
||||||
|
onChange={(v) => setPrecision(v ?? 2)}
|
||||||
|
style={{ width: 90 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Button icon={<DownloadOutlined />} loading={exporting} onClick={download}>
|
<Button icon={<DownloadOutlined />} loading={exporting} onClick={download}>
|
||||||
Экспорт
|
Экспорт
|
||||||
</Button>
|
</Button>
|
||||||
@ -163,6 +182,7 @@ function OsvContent({ versionId }: { versionId: string }) {
|
|||||||
filters={osv.filters}
|
filters={osv.filters}
|
||||||
members={(card?.members ?? []).map((m) => ({ id: m.id, name: m.name }))}
|
members={(card?.members ?? []).map((m) => ({ id: m.id, name: m.name }))}
|
||||||
onPage={osv.setPage}
|
onPage={osv.setPage}
|
||||||
|
numFormat={format}
|
||||||
onFilter={osv.patchFilters}
|
onFilter={osv.patchFilters}
|
||||||
onMap={setMapping}
|
onMap={setMapping}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Avatar, type TableColumnsType } from 'antd';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets';
|
import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets';
|
||||||
import type { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members';
|
import type { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members';
|
||||||
import { statusMapping } from './consts';
|
import { fileStatusMapping } from './consts';
|
||||||
|
|
||||||
/** Строка таблицы чек-листа: ЮЛ и статусы ББ, ОФР и ОСВ по счетам. */
|
/** Строка таблицы чек-листа: ЮЛ и статусы ББ, ОФР и ОСВ по счетам. */
|
||||||
export type ChecklistTableRow = {
|
export type ChecklistTableRow = {
|
||||||
@ -60,7 +60,7 @@ export function getMarksItems() {
|
|||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
}}>
|
}}>
|
||||||
<b>Условные обозначения</b>
|
<b>Условные обозначения</b>
|
||||||
{(Object.entries(statusMapping) as [FileStatus, string][]).map(([status, label]) => (
|
{(Object.entries(fileStatusMapping) as [FileStatus, string][]).map(([status, label]) => (
|
||||||
<div key={status}>
|
<div key={status}>
|
||||||
{mapStatus(status)} <span style={{ color: '#667085' }}>{label}</span>
|
{mapStatus(status)} <span style={{ color: '#667085' }}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,13 +2,12 @@ import { FileTextOutlined } from '@ant-design/icons';
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import type { TableConfig } from '../../components/GenericTable';
|
import type { TableConfig } from '../../components/GenericTable';
|
||||||
import { TableTag } from '../../components/TableTag/TableTag';
|
import { TableTag } from '../../components/TableTag/TableTag';
|
||||||
import { TAG_PRESETS } from '../../components/TableTag/presets';
|
|
||||||
import type { FileRussianStatus, MemberRow } from '../../types/consolidations/version_members';
|
import type { FileRussianStatus, MemberRow } from '../../types/consolidations/version_members';
|
||||||
import { statusColorMapping } from './consts';
|
import { fileStatusColorMapping } from './consts';
|
||||||
|
|
||||||
function StatusTag({ status }: { status: FileRussianStatus | undefined }) {
|
function StatusTag({ status }: { status: FileRussianStatus | undefined }) {
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
return <TableTag {...statusColorMapping[status]} text={status} />;
|
return <TableTag {...fileStatusColorMapping[status]} text={status} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const membersConfig: TableConfig<MemberRow> = {
|
export const membersConfig: TableConfig<MemberRow> = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets';
|
import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets';
|
||||||
import type { FileRussianStatus, FileStatus } from '../../types/consolidations/version_members';
|
import type { FileRussianStatus, FileStatus } from '../../types/consolidations/version_members';
|
||||||
|
|
||||||
export const statusMapping: Record<FileStatus, FileRussianStatus> = {
|
export const fileStatusMapping: Record<FileStatus, FileRussianStatus> = {
|
||||||
CONFIRMED: 'Загружен',
|
CONFIRMED: 'Загружен',
|
||||||
ERROR: 'Ошибка',
|
ERROR: 'Ошибка',
|
||||||
MISSING: 'Отсутствует',
|
MISSING: 'Отсутствует',
|
||||||
@ -10,7 +10,7 @@ export const statusMapping: Record<FileStatus, FileRussianStatus> = {
|
|||||||
RECOGNIZED: 'Распознан',
|
RECOGNIZED: 'Распознан',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const statusColorMapping: Record<FileRussianStatus, Preset> = {
|
export const fileStatusColorMapping: Record<FileRussianStatus, Preset> = {
|
||||||
'В обработке': TAG_PRESETS.geekblue,
|
'В обработке': TAG_PRESETS.geekblue,
|
||||||
Загружен: TAG_PRESETS.green,
|
Загружен: TAG_PRESETS.green,
|
||||||
Отсутствует: TAG_PRESETS.default,
|
Отсутствует: TAG_PRESETS.default,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FileRussianStatus, IVersionCard, MemberRow } from '../../types/consolidations/version_members';
|
import type { FileRussianStatus, IVersionCard, MemberRow } from '../../types/consolidations/version_members';
|
||||||
import { statusMapping } from './consts';
|
import { fileStatusMapping } from './consts';
|
||||||
|
|
||||||
export function normalizeData(cons_data: IVersionCard | null): MemberRow[] {
|
export function normalizeData(cons_data: IVersionCard | null): MemberRow[] {
|
||||||
if (cons_data === null) {
|
if (cons_data === null) {
|
||||||
@ -10,9 +10,9 @@ export function normalizeData(cons_data: IVersionCard | null): MemberRow[] {
|
|||||||
let ofr_status: FileRussianStatus | undefined;
|
let ofr_status: FileRussianStatus | undefined;
|
||||||
for (const c of m.checklist) {
|
for (const c of m.checklist) {
|
||||||
if (c.document_type === 'BALANCE') {
|
if (c.document_type === 'BALANCE') {
|
||||||
bb_status = statusMapping[c.status];
|
bb_status = fileStatusMapping[c.status];
|
||||||
} else if (c.document_type === 'INCOME') {
|
} else if (c.document_type === 'INCOME') {
|
||||||
ofr_status = statusMapping[c.status];
|
ofr_status = fileStatusMapping[c.status];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -208,7 +208,14 @@ export function ConsoleTableWidget() {
|
|||||||
|
|
||||||
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
|
||||||
// выбранная версия живёт в URL (?version=): переживает уход на «К отчётности» и «Назад»
|
// выбранная версия живёт в URL (?version=): переживает уход на «К отчётности» и «Назад»
|
||||||
onClick: () => setSearchParams({ version: row.id }, { replace: true }),
|
onClick: () =>
|
||||||
|
setSearchParams(
|
||||||
|
(searchParams) => {
|
||||||
|
searchParams.set('version', row.id);
|
||||||
|
return searchParams;
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
),
|
||||||
// выбранная версия подсвечена, как в макете [34]
|
// выбранная версия подсвечена, как в макете [34]
|
||||||
style: row.id === searchParams.get('version') ? { background: '#f0f7ee' } : undefined,
|
style: row.id === searchParams.get('version') ? { background: '#f0f7ee' } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -30,3 +30,12 @@ export const statusColorMapping: Record<ConsRussianStatus, Preset> = {
|
|||||||
Отклонён: TAG_PRESETS.red,
|
Отклонён: TAG_PRESETS.red,
|
||||||
Отменён: TAG_PRESETS.red,
|
Отменён: TAG_PRESETS.red,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const consStatusDescriptionMapping: Partial<Record<ConsStatus, string>> = {
|
||||||
|
CONSOLIDATION_RF: 'Формирование и проверка КФО, внесение корректировок на уровне РФ',
|
||||||
|
CONSOLIDATION_GO: 'Формирование и проверка КФО, внесение корректировок на уровне ГО',
|
||||||
|
VERIFICATION_GO: 'Проверка и подтверждение КФО на уровне ГО',
|
||||||
|
VERIFICATION_RF: 'Проверка и подтверждение КФО на уровне РФ',
|
||||||
|
CANCELLED: 'Формирование КФО отменено',
|
||||||
|
VERIFIED: 'КФО верифицирована, внесены номер и дата решения УОБ',
|
||||||
|
};
|
||||||
|
|||||||
@ -1,24 +1,23 @@
|
|||||||
import { FilterFilled } from '@ant-design/icons';
|
import { FilterFilled } from '@ant-design/icons';
|
||||||
import { Button, Checkbox, Input, Select, Space, Table } from 'antd';
|
import { Button, Checkbox, Input, Select, Space, Table } from 'antd';
|
||||||
import type { TableColumnsType } from 'antd';
|
import type { TableColumnsType } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { TableTag } from '../../components/TableTag/TableTag';
|
import { TableTag } from '../../components/TableTag/TableTag';
|
||||||
import type { ConsolidatedRowOut, CounterpartyType, OsvFilters } from '../../types/api';
|
import type { ConsolidatedRowOut, CounterpartyType, OsvFilters } from '../../types/api';
|
||||||
import { counterpartyTypeLabel, osvPeriodLabel } from '../../utils/labels';
|
import { counterpartyTypeLabel, osvPeriodLabel } from '../../utils/labels';
|
||||||
import { formatAmount } from '../../utils/money';
|
|
||||||
import { innCell, needsMapping } from './counterpartyMapping';
|
import { innCell, needsMapping } from './counterpartyMapping';
|
||||||
import { COUNTERPARTY_TYPES, counterpartyTypeBadge } from './counterpartyTypeBadge';
|
import { COUNTERPARTY_TYPES, counterpartyTypeBadge } from './counterpartyTypeBadge';
|
||||||
import type { OsvFilterDraft } from './osvFilterState';
|
import type { OsvFilterDraft } from './osvFilterState';
|
||||||
|
|
||||||
type MoneyKey = 'opening_debit' | 'opening_credit' | 'turnover_debit' | 'turnover_credit' | 'closing_debit' | 'closing_credit';
|
type MoneyKey = 'opening_debit' | 'opening_credit' | 'turnover_debit' | 'turnover_credit' | 'closing_debit' | 'closing_credit';
|
||||||
|
|
||||||
const money = (key: MoneyKey, title: string) => ({
|
const money = (key: MoneyKey, title: string, format: (raw: string | null) => string) => ({
|
||||||
key,
|
key,
|
||||||
title,
|
title,
|
||||||
dataIndex: key,
|
dataIndex: key,
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (v: string | null) => formatAmount(v, { zeroDash: true }),
|
render: (v: string | null) => format(v),
|
||||||
});
|
});
|
||||||
|
|
||||||
const group = (title: string, children: TableColumnsType<ConsolidatedRowOut>): TableColumnsType<ConsolidatedRowOut>[number] => ({
|
const group = (title: string, children: TableColumnsType<ConsolidatedRowOut>): TableColumnsType<ConsolidatedRowOut>[number] => ({
|
||||||
@ -195,6 +194,7 @@ type Props = {
|
|||||||
filters: OsvFilters;
|
filters: OsvFilters;
|
||||||
members: Member[];
|
members: Member[];
|
||||||
onPage: (p: number) => void;
|
onPage: (p: number) => void;
|
||||||
|
numFormat: (raw: string | null) => string;
|
||||||
onFilter: OnChange;
|
onFilter: OnChange;
|
||||||
/** «Указать» или ИНН сопоставленного ЮЛ — открыть выбор организации [32-osv-map-counterparty]. */
|
/** «Указать» или ИНН сопоставленного ЮЛ — открыть выбор организации [32-osv-map-counterparty]. */
|
||||||
onMap: (row: ConsolidatedRowOut) => void;
|
onMap: (row: ConsolidatedRowOut) => void;
|
||||||
@ -237,74 +237,91 @@ function InnCell({ row, onMap }: { row: ConsolidatedRowOut; onMap: (row: Consoli
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OsvTable({ rows, total, page, pageSize, loading, filters, members, onPage, onFilter, onMap }: Props) {
|
export function OsvTable({ rows, total, page, pageSize, loading, filters, members, numFormat, onPage, onFilter, onMap }: Props) {
|
||||||
const filtered = Object.keys(filters).length > 0;
|
const filtered = Object.keys(filters).length > 0;
|
||||||
const columns: TableColumnsType<ConsolidatedRowOut> = [
|
const columns: TableColumnsType<ConsolidatedRowOut> = useMemo(() => {
|
||||||
{
|
return [
|
||||||
key: 'legal_entity_name',
|
{
|
||||||
title: 'Компания ГК',
|
key: 'legal_entity_name',
|
||||||
dataIndex: 'legal_entity_name',
|
title: 'Компания ГК',
|
||||||
width: 230,
|
dataIndex: 'legal_entity_name',
|
||||||
fixed: 'left',
|
width: 230,
|
||||||
filterIcon: filterIcon(!!filters.legal_entity_id),
|
fixed: 'left',
|
||||||
// ключ в key выбирается, чтобы dropdown пересоздавался при внешнем сбросе фильтра
|
filterIcon: filterIcon(!!filters.legal_entity_id),
|
||||||
filterDropdown: ({ close }) => (
|
// ключ в key выбирается, чтобы dropdown пересоздавался при внешнем сбросе фильтра
|
||||||
<EntityFilter
|
filterDropdown: ({ close }) => (
|
||||||
key={filters.legal_entity_id ?? ''}
|
<EntityFilter
|
||||||
members={members}
|
key={filters.legal_entity_id ?? ''}
|
||||||
value={filters.legal_entity_id}
|
members={members}
|
||||||
onChange={onFilter}
|
value={filters.legal_entity_id}
|
||||||
close={close}
|
onChange={onFilter}
|
||||||
/>
|
close={close}
|
||||||
),
|
/>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
key: 'account_code',
|
{
|
||||||
title: 'Счёт',
|
key: 'account_code',
|
||||||
dataIndex: 'account_code',
|
title: 'Счёт',
|
||||||
width: 110,
|
dataIndex: 'account_code',
|
||||||
filterIcon: filterIcon(!!filters.account_root),
|
width: 110,
|
||||||
filterDropdown: ({ close }) => (
|
filterIcon: filterIcon(!!filters.account_root),
|
||||||
<AccountFilter key={filters.account_root ?? ''} value={filters.account_root} onChange={onFilter} close={close} />
|
filterDropdown: ({ close }) => (
|
||||||
),
|
<AccountFilter key={filters.account_root ?? ''} value={filters.account_root} onChange={onFilter} close={close} />
|
||||||
},
|
),
|
||||||
{ key: 'period', title: 'Период', dataIndex: 'period', width: 140, render: (p: string) => osvPeriodLabel(p) },
|
},
|
||||||
{
|
{ key: 'period', title: 'Период', dataIndex: 'period', width: 140, render: (p: string) => osvPeriodLabel(p) },
|
||||||
key: 'counterparty',
|
{
|
||||||
title: 'Контрагенты',
|
key: 'counterparty',
|
||||||
width: 260,
|
title: 'Контрагенты',
|
||||||
render: (_: unknown, r) => r.counterparty_entity_name ?? r.counterparty_name ?? '—',
|
width: 260,
|
||||||
filterIcon: filterIcon(!!filters.q),
|
render: (_: unknown, r) => r.counterparty_entity_name ?? r.counterparty_name ?? '—',
|
||||||
filterDropdown: ({ close }) => <CounterpartyFilter key={filters.q ?? ''} q={filters.q} onChange={onFilter} close={close} />,
|
filterIcon: filterIcon(!!filters.q),
|
||||||
},
|
filterDropdown: ({ close }) => <CounterpartyFilter key={filters.q ?? ''} q={filters.q} onChange={onFilter} close={close} />,
|
||||||
{
|
},
|
||||||
key: 'counterparty_inn',
|
{
|
||||||
title: 'ИНН',
|
key: 'counterparty_inn',
|
||||||
align: 'center',
|
title: 'ИНН',
|
||||||
// шире макетных 128: «таблетка» с 12-значным ИНН ИП
|
align: 'center',
|
||||||
width: 140,
|
// шире макетных 128: «таблетка» с 12-значным ИНН ИП
|
||||||
onCell: (r) => ({ style: needsMapping(r) ? REQUIRED_CELL : INN_CELL }),
|
width: 140,
|
||||||
render: (_: unknown, r) => <InnCell row={r} onMap={onMap} />,
|
onCell: (r) => ({ style: needsMapping(r) ? REQUIRED_CELL : INN_CELL }),
|
||||||
filterIcon: filterIcon(!!filters.unmapped),
|
render: (_: unknown, r) => <InnCell row={r} onMap={onMap} />,
|
||||||
filterDropdown: ({ close }) => (
|
filterIcon: filterIcon(!!filters.unmapped),
|
||||||
<UnmappedFilter key={String(!!filters.unmapped)} value={!!filters.unmapped} onChange={onFilter} close={close} />
|
filterDropdown: ({ close }) => (
|
||||||
),
|
<UnmappedFilter key={String(!!filters.unmapped)} value={!!filters.unmapped} onChange={onFilter} close={close} />
|
||||||
},
|
),
|
||||||
group('Сальдо на начало периода', [money('opening_debit', 'Дебет'), money('opening_credit', 'Кредит')]),
|
},
|
||||||
group('Обороты за период', [money('turnover_debit', 'Дебет'), money('turnover_credit', 'Кредит')]),
|
group('Сальдо на начало периода', [money('opening_debit', 'Дебет', numFormat), money('opening_credit', 'Кредит', numFormat)]),
|
||||||
group('Сальдо на конец периода', [money('closing_debit', 'Дебет'), money('closing_credit', 'Кредит')]),
|
group('Обороты за период', [money('turnover_debit', 'Дебет', numFormat), money('turnover_credit', 'Кредит', numFormat)]),
|
||||||
{
|
group('Сальдо на конец периода', [money('closing_debit', 'Дебет', numFormat), money('closing_credit', 'Кредит', numFormat)]),
|
||||||
key: 'counterparty_type',
|
{
|
||||||
title: 'Тип контрагента',
|
key: 'counterparty_type',
|
||||||
dataIndex: 'counterparty_type',
|
title: 'Тип контрагента',
|
||||||
width: 130,
|
dataIndex: 'counterparty_type',
|
||||||
render: (t: CounterpartyType) => <TypeBadge type={t} />,
|
width: 130,
|
||||||
filterIcon: filterIcon(!!filters.counterparty_type),
|
render: (t: CounterpartyType) => <TypeBadge type={t} />,
|
||||||
filterDropdown: ({ close }) => (
|
filterIcon: filterIcon(!!filters.counterparty_type),
|
||||||
<CounterpartyTypeFilter key={filters.counterparty_type ?? ''} type={filters.counterparty_type} onChange={onFilter} close={close} />
|
filterDropdown: ({ close }) => (
|
||||||
),
|
<CounterpartyTypeFilter
|
||||||
},
|
key={filters.counterparty_type ?? ''}
|
||||||
];
|
type={filters.counterparty_type}
|
||||||
|
onChange={onFilter}
|
||||||
|
close={close}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [
|
||||||
|
filters.account_root,
|
||||||
|
filters.counterparty_type,
|
||||||
|
filters.legal_entity_id,
|
||||||
|
filters.q,
|
||||||
|
filters.unmapped,
|
||||||
|
members,
|
||||||
|
numFormat,
|
||||||
|
onFilter,
|
||||||
|
onMap,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<Table<ConsolidatedRowOut>
|
<Table<ConsolidatedRowOut>
|
||||||
bordered
|
bordered
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export function OsvTabs() {
|
|||||||
return (
|
return (
|
||||||
<Segmented
|
<Segmented
|
||||||
value='ОСВ'
|
value='ОСВ'
|
||||||
style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto' }}
|
style={{ alignSelf: 'flex-start', maxWidth: '100%', overflowX: 'auto', overflowY: 'hidden' }}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'ОСВ', label: <span style={{ color: '#2a8c3a', fontWeight: 600 }}>ОСВ</span> },
|
{ value: 'ОСВ', label: <span style={{ color: '#2a8c3a', fontWeight: 600 }}>ОСВ</span> },
|
||||||
...TABS.map((t) => ({
|
...TABS.map((t) => ({
|
||||||
|
|||||||
42
web/src/widgets/ConsolidatedOsv/useNumberDisplay.tsx
Normal file
42
web/src/widgets/ConsolidatedOsv/useNumberDisplay.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
|
||||||
|
export type Unit = 'руб.' | 'тыс. руб.' | 'млн. руб.' | 'млрд. руб.';
|
||||||
|
|
||||||
|
export const UNITS: { value: Unit; label: string }[] = [
|
||||||
|
{ value: 'руб.', label: 'руб.' },
|
||||||
|
{ value: 'тыс. руб.', label: 'тыс. руб.' },
|
||||||
|
{ value: 'млн. руб.', label: 'млн. руб.' },
|
||||||
|
{ value: 'млрд. руб.', label: 'млрд. руб.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIVISORS: Record<Unit, number> = {
|
||||||
|
'руб.': 1,
|
||||||
|
'тыс. руб.': 1e3,
|
||||||
|
'млн. руб.': 1e6,
|
||||||
|
'млрд. руб.': 1e9,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createFormatter(precision: number): Intl.NumberFormat {
|
||||||
|
return new Intl.NumberFormat('ru-RU', {
|
||||||
|
minimumFractionDigits: precision,
|
||||||
|
maximumFractionDigits: precision,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNumeric(raw: string | null, unit: Unit, nf: Intl.NumberFormat): string {
|
||||||
|
if (raw == null || raw.trim() === '') return '—';
|
||||||
|
const n = Number(raw);
|
||||||
|
if (!Number.isFinite(n)) return raw;
|
||||||
|
return nf.format(n / DIVISORS[unit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNumberDisplay(initial?: { unit?: Unit; precision?: number }) {
|
||||||
|
const [unit, setUnit] = useState<Unit>(initial?.unit ?? 'руб.');
|
||||||
|
const [precision, setPrecision] = useState(initial?.precision ?? 2);
|
||||||
|
|
||||||
|
const nf = useMemo(() => createFormatter(precision), [precision]);
|
||||||
|
|
||||||
|
const format = useCallback((raw: string | null) => formatNumeric(raw, unit, nf), [unit, nf]);
|
||||||
|
|
||||||
|
return { unit, setUnit, precision, setPrecision, format };
|
||||||
|
}
|
||||||
@ -23,11 +23,6 @@ const errorCodeMap = {
|
|||||||
TRANSITION_NOT_ALLOWED: 'Данный переход недоступен',
|
TRANSITION_NOT_ALLOWED: 'Данный переход недоступен',
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
"Инициация", "Обработка документов", "Корректировка данных" -> "Отменена" (комментарий обязателен);
|
|
||||||
из "Обработка документов", "Корректировка данных" -> "Инициация" (комментарий обязателен во фронте)
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function mockStatusMappingFetch() {
|
async function mockStatusMappingFetch() {
|
||||||
const mapping: Record<ConsStatus, ConsStatus[]> = {
|
const mapping: Record<ConsStatus, ConsStatus[]> = {
|
||||||
INITIATED: ['CANCELLED'],
|
INITIATED: ['CANCELLED'],
|
||||||
@ -95,17 +90,19 @@ export default function ChangeStatusModal({ open, versionId, setClose, onSuccess
|
|||||||
Текущий этап: <TableTag text={russianStatus} {...badgePreset} />
|
Текущий этап: <TableTag text={russianStatus} {...badgePreset} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form form={form} layout='vertical' requiredMark={false} initialValues={{ comment: '', to_status: nextStatusOptions[0] }}>
|
{nextStatusOptions && (
|
||||||
<Form.Item name='to_status' label='Новый этап'>
|
<Form form={form} layout='vertical' requiredMark={false} initialValues={{ comment: '', to_status: nextStatusOptions[0] }}>
|
||||||
<Select
|
<Form.Item name='to_status' label='Новый этап'>
|
||||||
options={nextStatusOptions.map((i) => ({ label: i, value: i }))}
|
<Select
|
||||||
labelRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}
|
options={nextStatusOptions.map((i) => ({ label: i, value: i }))}
|
||||||
optionRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}></Select>
|
labelRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}
|
||||||
</Form.Item>
|
optionRender={(v) => (v.value ? statusMapping[v.value as ConsStatus] : v.value)}></Select>
|
||||||
<Form.Item name='comment' rules={[{ required: true, message: 'Комментарий необходим' }]} label='Комментарий'>
|
</Form.Item>
|
||||||
<Input.TextArea maxLength={1000} placeholder='Укажите комментарий к изменению статуса'></Input.TextArea>
|
<Form.Item name='comment' rules={[{ required: true, message: 'Комментарий необходим' }]} label='Комментарий'>
|
||||||
</Form.Item>
|
<Input.TextArea maxLength={1000} placeholder='Укажите комментарий к изменению статуса'></Input.TextArea>
|
||||||
</Form>
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user