Add consolidation POST + file upload
This commit is contained in:
parent
81548eb4ae
commit
8747f6318c
@ -33,6 +33,9 @@ interface DataTableProps<T extends Record<string, any>> {
|
|||||||
scrollY?: number | string;
|
scrollY?: number | string;
|
||||||
onRow?: TableProps<T>["onRow"];
|
onRow?: TableProps<T>["onRow"];
|
||||||
}
|
}
|
||||||
|
const locale = {
|
||||||
|
emptyText: "Нет данных, чтобы отобразить",
|
||||||
|
};
|
||||||
|
|
||||||
export function DataTable<T extends Record<string, any>>({
|
export function DataTable<T extends Record<string, any>>({
|
||||||
config,
|
config,
|
||||||
@ -209,6 +212,7 @@ export function DataTable<T extends Record<string, any>>({
|
|||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} style={{ flex: 1, height: "100%" }}>
|
<div ref={wrapperRef} style={{ flex: 1, height: "100%" }}>
|
||||||
<Table<T>
|
<Table<T>
|
||||||
|
locale={locale}
|
||||||
rowKey={rowKey}
|
rowKey={rowKey}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
47
src/components/TableTag/TableTag.tsx
Normal file
47
src/components/TableTag/TableTag.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type TableTagProps = {
|
||||||
|
text: string;
|
||||||
|
color?: string; // text color
|
||||||
|
bgColor?: string; // background color
|
||||||
|
borderColor?: string; // border color
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
rounded?: boolean | string; // true = fully rounded
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableTag: React.FC<TableTagProps> = ({
|
||||||
|
text,
|
||||||
|
color = "#1677ff",
|
||||||
|
bgColor = "#e6f4ff",
|
||||||
|
borderColor = "#91caff",
|
||||||
|
icon,
|
||||||
|
rounded = true,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
padding: "2px 10px",
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: "20px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: color,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
border: `1px solid ${borderColor}`,
|
||||||
|
borderRadius: rounded === true ? 999 : (rounded as string),
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon && <span style={{ display: "inline-flex" }}>{icon}</span>}
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
42
src/components/TableTag/presets.tsx
Normal file
42
src/components/TableTag/presets.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export type Preset = {
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
borderColor: string;
|
||||||
|
};
|
||||||
|
export const TAG_PRESETS = {
|
||||||
|
green: {
|
||||||
|
color: "rgb(22, 101, 52)",
|
||||||
|
bgColor: "rgb(236, 253, 243)",
|
||||||
|
borderColor: "rgba(58, 150, 71, 0.2)",
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
color: "rgb(153, 27, 27)",
|
||||||
|
bgColor: "rgb(254, 226, 226)",
|
||||||
|
borderColor: "rgba(239, 68, 68, 0.2)",
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
color: "rgb(154, 52, 18)",
|
||||||
|
bgColor: "rgb(255, 237, 213)",
|
||||||
|
borderColor: "rgba(251, 146, 60, 0.28)",
|
||||||
|
},
|
||||||
|
geekblue: {
|
||||||
|
color: "rgb(36, 86, 197)",
|
||||||
|
bgColor: "rgb(238, 244, 255)",
|
||||||
|
borderColor: "rgba(37, 99, 235, 0.2)",
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
bgColor: "rgb(237, 233, 254)",
|
||||||
|
borderColor: "rgba(124, 58, 237, 0.2)",
|
||||||
|
color: "rgb(91, 33, 182)",
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
bgColor: "rgb(241, 245, 249)",
|
||||||
|
borderColor: "rgba(0, 0, 0, 0.08)",
|
||||||
|
color: "rgb(102, 112, 133)",
|
||||||
|
},
|
||||||
|
gold: {
|
||||||
|
bgColor: "rgb(254, 243, 199)",
|
||||||
|
borderColor: "rgba(245, 158, 11, 0.25)",
|
||||||
|
color: "rgb(146, 64, 14)",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@ -16,30 +16,8 @@ import PageTitle from "../components/PageTitle";
|
|||||||
import { fetchOneCompanyGroup } from "../services/api/directories/company_groups";
|
import { fetchOneCompanyGroup } from "../services/api/directories/company_groups";
|
||||||
import { getConsolidationsListThunk } from "../store/slices/consolidations/thunks";
|
import { getConsolidationsListThunk } from "../store/slices/consolidations/thunks";
|
||||||
import { ConsoleTableWidget } from "../widgets/ConsoleTable/ConsoleTable";
|
import { ConsoleTableWidget } from "../widgets/ConsoleTable/ConsoleTable";
|
||||||
|
import { setSelectedVersion } from "../store/slices/consolidations/consolidationsSlice";
|
||||||
function DrawerContent() {
|
import { DocumentsDrawer } from "../widgets/FilesDrawer/FilesDrawer";
|
||||||
const { current_version_id, current_legal_entity_id } = useAppSelector(
|
|
||||||
(state) => state.consolidations,
|
|
||||||
);
|
|
||||||
const [drawerContent, setDrawerContent] = useState([]);
|
|
||||||
// if (!drawerContent.length) return;
|
|
||||||
if (!current_legal_entity_id) return;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
Version_id: {current_version_id}, legal_entity_id:{" "}
|
|
||||||
{current_legal_entity_id}
|
|
||||||
</div>
|
|
||||||
<FileUploaderModal
|
|
||||||
legal_entity_id={current_legal_entity_id}
|
|
||||||
trigger_sse_cb={() => {}}
|
|
||||||
/>
|
|
||||||
{drawerContent.map((f) => (
|
|
||||||
<FileItem key={f.id} file={f} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConsolidationPage() {
|
export function ConsolidationPage() {
|
||||||
const [legalEntities, setLegalEntities] = useState([]);
|
const [legalEntities, setLegalEntities] = useState([]);
|
||||||
@ -65,6 +43,9 @@ export function ConsolidationPage() {
|
|||||||
// dispatch()
|
// dispatch()
|
||||||
}
|
}
|
||||||
getAllInfo();
|
getAllInfo();
|
||||||
|
return () => {
|
||||||
|
dispatch(setSelectedVersion(null));
|
||||||
|
};
|
||||||
}, [dispatch, group_id]);
|
}, [dispatch, group_id]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -124,15 +105,12 @@ export function ConsolidationPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Drawer
|
<DocumentsDrawer
|
||||||
size="large"
|
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<DrawerContent />
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Select } from "antd";
|
|||||||
import PdfDataViewer from "../widgets/PDFResultViewer/PDFDataViewer";
|
import PdfDataViewer from "../widgets/PDFResultViewer/PDFDataViewer";
|
||||||
import { Config } from "../conf/config";
|
import { Config } from "../conf/config";
|
||||||
import IFrameBlock from "../widgets/PDFResultViewer/IFrameBlock";
|
import IFrameBlock from "../widgets/PDFResultViewer/IFrameBlock";
|
||||||
import { getFilesThunk } from "../store/slices/fileSlice";
|
import { getFilesThunk } from "../store/slices/files/thunks";
|
||||||
|
|
||||||
function PdfViewer({ fileId }: { fileId: string }) {
|
function PdfViewer({ fileId }: { fileId: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,14 +2,28 @@ import { api } from "../../api/client";
|
|||||||
import { MOCK_CONS, versionCardsMock } from "./mocks";
|
import { MOCK_CONS, versionCardsMock } from "./mocks";
|
||||||
|
|
||||||
export async function getConsolidations(company_group_id: string) {
|
export async function getConsolidations(company_group_id: string) {
|
||||||
// const res = await api.get(
|
const res = await api.get(
|
||||||
// `/company-groups/${company_group_id}/consolidations`,
|
`/company-groups/${company_group_id}/consolidations`,
|
||||||
// );
|
);
|
||||||
|
return res;
|
||||||
|
|
||||||
return MOCK_CONS.map((c) => ({ ...c, versions: [...c.versions] }));
|
// return MOCK_CONS.map((c) => ({ ...c, versions: [...c.versions] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVersionData(version_id: string) {
|
export async function getVersionData(version_id: string) {
|
||||||
const res = versionCardsMock[version_id] ?? null;
|
// const res = versionCardsMock[version_id] ?? null;
|
||||||
|
const res = await api.get(`/consolidations/versions/${version_id}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
type FormValues = {
|
||||||
|
company_group_id: string;
|
||||||
|
related_group_id: string;
|
||||||
|
year: number;
|
||||||
|
quarter: string; // example of 2nd static field
|
||||||
|
legal_entity_ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createConsolidation(values: FormValues) {
|
||||||
|
const res = await api.post("/consolidations", values);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,3 +20,22 @@ export async function fetchGSKByComGroup(com_group_id: string) {
|
|||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export async function fetchGroupMembers(related_group_id: string) {
|
||||||
|
// const res = await api.get(`/directories/related-groups/${related_group_id}`);
|
||||||
|
// const members = res?.members ?? [];
|
||||||
|
// console.log(members);
|
||||||
|
// return members;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// legal-entities?company_group_id=<ГК>&closed=false
|
||||||
|
|
||||||
|
export async function fetchGroupMembers(company_group_id: string) {
|
||||||
|
const res = await api.get(
|
||||||
|
`/directories/legal-entities?company_group_id=${company_group_id}&closed=false`,
|
||||||
|
);
|
||||||
|
console.log(res);
|
||||||
|
const members = res?.items ?? [];
|
||||||
|
console.log(members);
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|||||||
27
src/services/api/files.ts
Normal file
27
src/services/api/files.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { api, apiFetch } from "../../api/client";
|
||||||
|
|
||||||
|
export async function uploadOneFile(
|
||||||
|
legal_entity_id: string,
|
||||||
|
file: any,
|
||||||
|
consolidation_version_id: string | null,
|
||||||
|
) {
|
||||||
|
console.log("DATA", file);
|
||||||
|
console.log(legal_entity_id);
|
||||||
|
console.log(consolidation_version_id);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("files", file);
|
||||||
|
formData.append("legal_entity_id", legal_entity_id);
|
||||||
|
formData.append("year", "2025");
|
||||||
|
formData.append("quarter", "Q3");
|
||||||
|
console.log(formData);
|
||||||
|
apiFetch(
|
||||||
|
`/legal-entities/${legal_entity_id}/files?consolidation_version_id=${consolidation_version_id}`,
|
||||||
|
{ method: "POST", body: formData },
|
||||||
|
);
|
||||||
|
// if (!res.ok) throw new Error((await res.text()) || "Upload error");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLegalEntityFiles(legal_entity_id: string) {
|
||||||
|
const res = api.get(`/legal-entities/${legal_entity_id}/files`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { Config } from "../../conf/config";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
currentOpenFiles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFilesThunk = createAsyncThunk(
|
|
||||||
"files/getCurrentFiles",
|
|
||||||
async (legalEntityId, thunkApi) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${Config.API_URL}/files/by-legal-entity?legal_entity_id=${legalEntityId}`,
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error("Error in fetch");
|
|
||||||
const json = await res.json();
|
|
||||||
return thunkApi.fulfillWithValue(json);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const filesSlice = createSlice({
|
|
||||||
name: "files",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setCurrentFiles: (state, action) => {
|
|
||||||
state.currentOpenFiles = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers(builder) {
|
|
||||||
builder.addCase(getFilesThunk.fulfilled, (state, action) => {
|
|
||||||
state.currentOpenFiles = action.payload;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setCurrentFiles } = filesSlice.actions;
|
|
||||||
export default filesSlice.reducer;
|
|
||||||
39
src/store/slices/files/fileSlice.ts
Normal file
39
src/store/slices/files/fileSlice.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { Config } from "../../../conf/config";
|
||||||
|
import { getFilesThunk } from "./thunks";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
currentOpenFiles: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filesSlice = createSlice({
|
||||||
|
name: "files",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCurrentFiles: (state, action) => {
|
||||||
|
state.currentOpenFiles = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers(builder) {
|
||||||
|
builder
|
||||||
|
.addCase(getFilesThunk.pending, (state, action) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getFilesThunk.fulfilled, (state, action) => {
|
||||||
|
state.currentOpenFiles = action.payload;
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(getFilesThunk.rejected, (state, action) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
state.currentOpenFiles = [];
|
||||||
|
state.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setCurrentFiles } = filesSlice.actions;
|
||||||
|
export default filesSlice.reducer;
|
||||||
11
src/store/slices/files/thunks.ts
Normal file
11
src/store/slices/files/thunks.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
|
import { fetchLegalEntityFiles } from "../../../services/api/files";
|
||||||
|
|
||||||
|
export const getFilesThunk = createAsyncThunk(
|
||||||
|
"files/getCurrentFiles",
|
||||||
|
async (legalEntityId: string, thunkApi) => {
|
||||||
|
const res = await fetchLegalEntityFiles(legalEntityId);
|
||||||
|
console.log("RESULT", res);
|
||||||
|
return thunkApi.fulfillWithValue(res.items);
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import filesReducer from "./slices/fileSlice";
|
import filesReducer from "./slices/files/fileSlice";
|
||||||
import comGroupsReducer from "./slices/companyGroupsSlice/companyGroupsSlice";
|
import comGroupsReducer from "./slices/companyGroupsSlice/companyGroupsSlice";
|
||||||
import consolidationsReducer from "./slices/consolidations/consolidationsSlice";
|
import consolidationsReducer from "./slices/consolidations/consolidationsSlice";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|||||||
@ -19,7 +19,7 @@ interface IConsolidationVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IConsolidation {
|
export interface IConsolidation {
|
||||||
id: string;
|
consolidation_id: string;
|
||||||
year: number;
|
year: number;
|
||||||
quarter: QuarterEnum;
|
quarter: QuarterEnum;
|
||||||
reporting_date: string;
|
reporting_date: string;
|
||||||
|
|||||||
@ -28,7 +28,14 @@ type VersionHistory = {
|
|||||||
comment: string | null;
|
comment: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileStatus =
|
export type DocumentType =
|
||||||
|
| "BALANCE"
|
||||||
|
| "INCOME"
|
||||||
|
| "OSV"
|
||||||
|
| "DISCLOSURE"
|
||||||
|
| "OTHER";
|
||||||
|
|
||||||
|
export type FileStatus =
|
||||||
| "CONFIRMED"
|
| "CONFIRMED"
|
||||||
| "NEEDS_CHECK"
|
| "NEEDS_CHECK"
|
||||||
| "RECOGNIZED"
|
| "RECOGNIZED"
|
||||||
@ -36,10 +43,20 @@ type FileStatus =
|
|||||||
| "ERROR"
|
| "ERROR"
|
||||||
| "MISSING";
|
| "MISSING";
|
||||||
|
|
||||||
|
export type FileRussianStatus =
|
||||||
|
| "Требуется проверка"
|
||||||
|
| "Ошибка"
|
||||||
|
| "Отсутствует"
|
||||||
|
| "Загружен"
|
||||||
|
| "В обработке"
|
||||||
|
| "Распознан";
|
||||||
|
|
||||||
type ChecklistItem = {
|
type ChecklistItem = {
|
||||||
document_ids: string[];
|
document_ids: string[];
|
||||||
|
document_type: DocumentType;
|
||||||
status: FileStatus;
|
status: FileStatus;
|
||||||
missing_quarters: QuarterEnum[];
|
missing_quarters: QuarterEnum[];
|
||||||
|
account?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VersionMember = {
|
type VersionMember = {
|
||||||
@ -48,6 +65,7 @@ type VersionMember = {
|
|||||||
inn: string;
|
inn: string;
|
||||||
is_head_entity: string;
|
is_head_entity: string;
|
||||||
checklist: ChecklistItem[];
|
checklist: ChecklistItem[];
|
||||||
|
entity_type: "ИП" | "ЮЛ";
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionCard {
|
export interface IVersionCard {
|
||||||
@ -78,4 +96,8 @@ export type VersionCardRow = {
|
|||||||
inn: string;
|
inn: string;
|
||||||
name: string;
|
name: string;
|
||||||
version_id: string;
|
version_id: string;
|
||||||
|
bb_status: FileRussianStatus;
|
||||||
|
uploaded_osvs: string;
|
||||||
|
ofr_status: FileRussianStatus;
|
||||||
|
entity_type: "ЮЛ" | "ИП";
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { Tag } from "antd";
|
|
||||||
import type { TableConfig } from "../../components/GenericTable";
|
import type { TableConfig } from "../../components/GenericTable";
|
||||||
import { ICompanyGroup } from "../../types/companyGroups/companyGroups";
|
import { ICompanyGroup } from "../../types/companyGroups/companyGroups";
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import {
|
|||||||
VersionCardRow,
|
VersionCardRow,
|
||||||
} from "../../types/consolidations/version_members";
|
} from "../../types/consolidations/version_members";
|
||||||
import { FileOutlined, FileTextOutlined } from "@ant-design/icons";
|
import { FileOutlined, FileTextOutlined } from "@ant-design/icons";
|
||||||
|
import { TableTag } from "../../components/TableTag/TableTag";
|
||||||
|
import { statusColorMapping } from "./consts";
|
||||||
|
import { TAG_PRESETS } from "../../components/TableTag/presets";
|
||||||
|
|
||||||
export const membersConfig: TableConfig<
|
export const membersConfig: TableConfig<
|
||||||
VersionCardRow & { docs: "Документы" }
|
VersionCardRow & { docs: "Документы" }
|
||||||
@ -27,6 +30,27 @@ export const membersConfig: TableConfig<
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "ofr_status",
|
||||||
|
title: "ОФР",
|
||||||
|
features: { multiselect: true },
|
||||||
|
render: (v) => <TableTag {...statusColorMapping[v]} text={v} />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: "bb_status",
|
||||||
|
title: "Бух.баланс",
|
||||||
|
features: { multiselect: true },
|
||||||
|
render: (v) => <TableTag {...statusColorMapping[v]} text={v} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "uploaded_osvs",
|
||||||
|
title: "ОСВ",
|
||||||
|
features: { multiselect: true },
|
||||||
|
render: (v) => <TableTag {...TAG_PRESETS.default} text={v} />,
|
||||||
|
},
|
||||||
{ key: "inn", title: "ИНН", features: { search: true, multiselect: true } },
|
{ key: "inn", title: "ИНН", features: { search: true, multiselect: true } },
|
||||||
|
|
||||||
|
{ key: "entity_type", title: "Тип", features: { multiselect: true } },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/widgets/ConsoleLegalEntities/consts.ts
Normal file
23
src/widgets/ConsoleLegalEntities/consts.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Preset, TAG_PRESETS } from "../../components/TableTag/presets";
|
||||||
|
import {
|
||||||
|
FileRussianStatus,
|
||||||
|
FileStatus,
|
||||||
|
} from "../../types/consolidations/version_members";
|
||||||
|
|
||||||
|
export const statusMapping: Record<FileStatus, FileRussianStatus> = {
|
||||||
|
CONFIRMED: "Загружен",
|
||||||
|
ERROR: "Ошибка",
|
||||||
|
MISSING: "Отсутствует",
|
||||||
|
NEEDS_CHECK: "Требуется проверка",
|
||||||
|
PROCESSING: "В обработке",
|
||||||
|
RECOGNIZED: "Распознан",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusColorMapping: Record<FileRussianStatus, Preset> = {
|
||||||
|
"В обработке": TAG_PRESETS.geekblue,
|
||||||
|
Загружен: TAG_PRESETS.green,
|
||||||
|
Отсутствует: TAG_PRESETS.red,
|
||||||
|
Ошибка: TAG_PRESETS.red,
|
||||||
|
Распознан: TAG_PRESETS.gold,
|
||||||
|
"Требуется проверка": TAG_PRESETS.orange,
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import {
|
|||||||
IVersionCard,
|
IVersionCard,
|
||||||
VersionCardRow,
|
VersionCardRow,
|
||||||
} from "../../types/consolidations/version_members";
|
} from "../../types/consolidations/version_members";
|
||||||
|
import { statusMapping } from "./consts";
|
||||||
|
|
||||||
export function normalizeData(
|
export function normalizeData(
|
||||||
cons_data: IVersionCard | null,
|
cons_data: IVersionCard | null,
|
||||||
@ -10,13 +11,30 @@ export function normalizeData(
|
|||||||
if (cons_data === null) {
|
if (cons_data === null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
console.log(cons_data);
|
||||||
for (const m of cons_data.members) {
|
for (const m of cons_data.members) {
|
||||||
result.push({
|
let row: VersionCardRow = {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
inn: m.inn,
|
inn: m.inn,
|
||||||
version_id: cons_data.id,
|
version_id: cons_data.id,
|
||||||
});
|
entity_type: m.entity_type,
|
||||||
|
};
|
||||||
|
let uploaded_osvs = [];
|
||||||
|
for (let c of m.checklist) {
|
||||||
|
if (c.document_type === "BALANCE") {
|
||||||
|
row.bb_status = statusMapping[c.status];
|
||||||
|
} else if (c.document_type === "INCOME") {
|
||||||
|
row.ofr_status = statusMapping[c.status];
|
||||||
|
} else if (c.document_type === "OSV") {
|
||||||
|
if (c.status !== "MISSING") {
|
||||||
|
uploaded_osvs.push(c.account);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.uploaded_osvs = [...uploaded_osvs].sort().join(", ");
|
||||||
|
result.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,9 +169,7 @@ export function ConsoleTableWidget() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
onClick={() =>
|
onClick={() => navigate(`/cons/${row.con_id}/versions/${row.id}`)}
|
||||||
navigate(`/cons/${row.group_id}/versions/${row.id}`)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
К отчетности <ArrowRightOutlined />
|
К отчетности <ArrowRightOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,16 +1,8 @@
|
|||||||
import { Tag } from "antd";
|
|
||||||
import { TableConfig } from "../../components/GenericTable";
|
import { TableConfig } from "../../components/GenericTable";
|
||||||
import { ConsRow, LockOut } from "../../types/consolidations/consolidations";
|
import { ConsRow, LockOut } from "../../types/consolidations/consolidations";
|
||||||
import { statusColorMapping } from "./consts";
|
import { statusColorMapping } from "./consts";
|
||||||
|
import { TableTag } from "../../components/TableTag/TableTag";
|
||||||
const statusColor: Record<string, string> = {
|
import { TAG_PRESETS } from "../../components/TableTag/presets";
|
||||||
INITIATED: "gold",
|
|
||||||
DOCUMENTS_PROCESSING: "blue",
|
|
||||||
VERIFIED: "green",
|
|
||||||
CANCELLED: "default",
|
|
||||||
REJECTED: "red",
|
|
||||||
ARCHIVED: "default",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const consConfigBase: TableConfig<ConsRow> = {
|
export const consConfigBase: TableConfig<ConsRow> = {
|
||||||
rowKey: "id",
|
rowKey: "id",
|
||||||
@ -24,11 +16,7 @@ export const consConfigBase: TableConfig<ConsRow> = {
|
|||||||
key: "status",
|
key: "status",
|
||||||
title: "Статус",
|
title: "Статус",
|
||||||
features: { multiselect: true, sort: true },
|
features: { multiselect: true, sort: true },
|
||||||
render: (v) => (
|
render: (v) => <TableTag {...statusColorMapping[v]} text={v as string} />,
|
||||||
<Tag color={statusColorMapping[v]} variant="outlined">
|
|
||||||
{v}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "reporting_date",
|
key: "reporting_date",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { TAG_PRESETS, type Preset } from "../../components/TableTag/presets";
|
||||||
import {
|
import {
|
||||||
ConsRussianStatus,
|
ConsRussianStatus,
|
||||||
ConsStatus,
|
ConsStatus,
|
||||||
@ -18,17 +19,17 @@ export const statusMapping: Record<ConsStatus, ConsRussianStatus> = {
|
|||||||
REJECTED: "Отклонён",
|
REJECTED: "Отклонён",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const statusColorMapping: Record<ConsRussianStatus, string> = {
|
export const statusColorMapping: Record<ConsRussianStatus, Preset> = {
|
||||||
"В архиве": "default",
|
"В архиве": TAG_PRESETS.default,
|
||||||
"Верификация ГО": "geekblue",
|
"Верификация ГО": TAG_PRESETS.geekblue,
|
||||||
"Верификация РФ": "geekblue",
|
"Верификация РФ": TAG_PRESETS.geekblue,
|
||||||
Верифицирован: "green",
|
Верифицирован: TAG_PRESETS.green,
|
||||||
Инициация: "default",
|
Инициация: TAG_PRESETS.default,
|
||||||
"Консолидация ГО": "purple",
|
"Консолидация ГО": TAG_PRESETS.purple,
|
||||||
"Консолидация РФ": "purple",
|
"Консолидация РФ": TAG_PRESETS.purple,
|
||||||
Корректировка: "gold",
|
Корректировка: TAG_PRESETS.gold,
|
||||||
"Коррекция данных": "gold",
|
"Коррекция данных": TAG_PRESETS.gold,
|
||||||
"Обработка документов": "gold",
|
"Обработка документов": TAG_PRESETS.gold,
|
||||||
Отклонён: "red",
|
Отклонён: TAG_PRESETS.red,
|
||||||
Отменён: "red",
|
Отменён: TAG_PRESETS.red,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,11 +16,11 @@ export function flattenCons(cons: IConsolidation[]): ConsRow[] {
|
|||||||
let rep_date = new Date(con.reporting_date) ?? null;
|
let rep_date = new Date(con.reporting_date) ?? null;
|
||||||
out.push({
|
out.push({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
group_id: con.id,
|
group_id: con.consolidation_id,
|
||||||
isPrimary: i === 0,
|
isPrimary: i === 0,
|
||||||
versionIndex: i,
|
versionIndex: i,
|
||||||
groupSize: con.versions.length,
|
groupSize: con.versions.length,
|
||||||
con_id: con.id,
|
con_id: con.consolidation_id,
|
||||||
year: con.year,
|
year: con.year,
|
||||||
quarter: con.quarter,
|
quarter: con.quarter,
|
||||||
reporting_date: rep_date,
|
reporting_date: rep_date,
|
||||||
|
|||||||
62
src/widgets/FilesDrawer/FilesDrawer.tsx
Normal file
62
src/widgets/FilesDrawer/FilesDrawer.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// DocumentsDrawer.tsx
|
||||||
|
import { Drawer, List, Button } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useDocumentsPolling } from "./useDocumentsPolling";
|
||||||
|
import { UploadDocumentsModal } from "./UploadDocumentsModal";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../store/store";
|
||||||
|
import { getFilesThunk } from "../../store/slices/files/thunks";
|
||||||
|
|
||||||
|
export function DocumentsDrawer({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [uploadOpen, setUploadOpen] = useState(false);
|
||||||
|
const { current_version_id, current_legal_entity_id } = useAppSelector(
|
||||||
|
(state) => state.consolidations,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, loading } = useDocumentsPolling(current_legal_entity_id, open);
|
||||||
|
const handleClose = () => {
|
||||||
|
// optional: clear on close so next open doesn't flash stale list
|
||||||
|
// dispatch(clearDocuments());
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
destroyOnClose
|
||||||
|
title="Документы"
|
||||||
|
extra={
|
||||||
|
<Button type="primary" onClick={() => setUploadOpen(true)}>
|
||||||
|
Upload documents
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
loading={loading && !data.length}
|
||||||
|
dataSource={data}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item>
|
||||||
|
{item.original_name} - {item.file_type} - {item.status}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{current_legal_entity_id && (
|
||||||
|
<UploadDocumentsModal
|
||||||
|
entityId={current_legal_entity_id}
|
||||||
|
consolidation_version_id={current_version_id}
|
||||||
|
open={uploadOpen}
|
||||||
|
onClose={() => setUploadOpen(false)}
|
||||||
|
onUploaded={() => dispatch(getFilesThunk(current_legal_entity_id))} // instant refresh, polling covers steady state
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/widgets/FilesDrawer/UploadDocumentsModal.tsx
Normal file
104
src/widgets/FilesDrawer/UploadDocumentsModal.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// UploadDocumentsModal.tsx
|
||||||
|
import { Modal, Upload, message } from "antd";
|
||||||
|
import { InboxOutlined } from "@ant-design/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { UploadFile, RcFile } from "antd/es/upload/interface";
|
||||||
|
import { uploadOneFile } from "../../services/api/files";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entityId: string;
|
||||||
|
open: boolean;
|
||||||
|
consolidation_version_id: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onUploaded?: () => void; // trigger refetch after done
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UploadDocumentsModal({
|
||||||
|
entityId,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onUploaded,
|
||||||
|
consolidation_version_id = null,
|
||||||
|
}: Props) {
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (!fileList.length) {
|
||||||
|
message.warning("No files selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
|
// one-by-one (sequential)
|
||||||
|
for (const file of fileList) {
|
||||||
|
const raw = (file.originFileObj ?? file) as RcFile;
|
||||||
|
try {
|
||||||
|
await uploadOneFile(entityId, raw, consolidation_version_id);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push(file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(false);
|
||||||
|
|
||||||
|
if (failed.length) {
|
||||||
|
message.error(`Failed: ${failed.join(", ")}`);
|
||||||
|
// keep only failed files so user can retry
|
||||||
|
setFileList((prev) => prev.filter((f) => failed.includes(f.name)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success("All documents uploaded");
|
||||||
|
setFileList([]);
|
||||||
|
onUploaded?.(); // refetch -> polling will also catch up, but immediate feels better
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (uploading) return; // block closing mid-upload
|
||||||
|
setFileList([]);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Загрузить документы"
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
okText="Upload"
|
||||||
|
confirmLoading={uploading}
|
||||||
|
okButtonProps={{ disabled: !fileList.length }}
|
||||||
|
closable={!uploading}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Upload.Dragger
|
||||||
|
multiple
|
||||||
|
fileList={fileList}
|
||||||
|
accept=".xlsx,.xls,.pdf,.jpg,.jpeg"
|
||||||
|
beforeUpload={(file) => {
|
||||||
|
// dedupe by uid protects from React StrictMode double invoke
|
||||||
|
setFileList((prev) =>
|
||||||
|
prev.some((f) => f.uid === file.uid)
|
||||||
|
? prev
|
||||||
|
: [...prev, file as UploadFile],
|
||||||
|
);
|
||||||
|
return false; // prevent antd auto-upload
|
||||||
|
}}
|
||||||
|
onRemove={(file) => {
|
||||||
|
setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
|
||||||
|
}}
|
||||||
|
// we manage fileList ourselves, so disable antd status updates
|
||||||
|
showUploadList={{ showRemoveIcon: !uploading }}
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">Выберите файлы</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/widgets/FilesDrawer/useDocumentsPolling.ts
Normal file
27
src/widgets/FilesDrawer/useDocumentsPolling.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// useDocumentsPolling.ts
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../store/store";
|
||||||
|
import { getFilesThunk } from "../../store/slices/files/thunks";
|
||||||
|
|
||||||
|
const POLL_INTERVAL = 5000;
|
||||||
|
|
||||||
|
export function useDocumentsPolling(rowId: string | null, enabled: boolean) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const data = useAppSelector((s) => s.files.currentOpenFiles);
|
||||||
|
const loading = useAppSelector((s) => s.files.loading);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !rowId) return;
|
||||||
|
|
||||||
|
// immediate fetch
|
||||||
|
dispatch(getFilesThunk(rowId));
|
||||||
|
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
dispatch(getFilesThunk(rowId));
|
||||||
|
}, POLL_INTERVAL);
|
||||||
|
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [dispatch, rowId, enabled]);
|
||||||
|
|
||||||
|
return { data, loading };
|
||||||
|
}
|
||||||
@ -1,14 +1,28 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Modal, Form, Select, Row, Col } from "antd";
|
import {
|
||||||
import { fetchGSKByComGroup } from "../../services/api/directories/company_groups";
|
Modal,
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
fetchGroupMembers,
|
||||||
|
fetchGSKByComGroup,
|
||||||
|
} from "../../services/api/directories/company_groups";
|
||||||
import { useAppDispatch, useAppSelector } from "../../store/store";
|
import { useAppDispatch, useAppSelector } from "../../store/store";
|
||||||
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
import { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
groupId: string;
|
company_group_id: string;
|
||||||
subGroupId: string;
|
related_group_id: string;
|
||||||
year: number;
|
year: number;
|
||||||
quarter: string; // example of 2nd static field
|
quarter: string; // example of 2nd static field
|
||||||
|
legal_entity_ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -23,27 +37,21 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
|
|
||||||
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
|
const { raw_rows, loading } = useAppSelector((state) => state.company_groups);
|
||||||
|
|
||||||
// Subgroups are local — only this modal needs them
|
const [subgroups, setSubgroups] = useState<any[]>([]);
|
||||||
const [subgroups, setSubgroups] = useState([]);
|
|
||||||
const [loadingSubgroups, setLoadingSubgroups] = useState(false);
|
const [loadingSubgroups, setLoadingSubgroups] = useState(false);
|
||||||
// Guards against out-of-order responses when group is changed quickly
|
const subgroupReqRef = useRef(0);
|
||||||
const requestIdRef = useRef(0);
|
|
||||||
|
|
||||||
const groupId = Form.useWatch("groupId", form);
|
const [legEntities, setLegEntities] = useState<any[]>([]);
|
||||||
const subGroupId = Form.useWatch("subGroupId", form);
|
const [loadingEntities, setLoadingEntities] = useState(false);
|
||||||
|
const legEntReqRef = useRef(0);
|
||||||
|
|
||||||
|
const [legEntSearch, setLegEntSearch] = useState("");
|
||||||
|
|
||||||
|
const company_group_id = Form.useWatch("company_group_id", form);
|
||||||
|
const related_group_id = Form.useWatch("related_group_id", form);
|
||||||
const year = Form.useWatch("year", form);
|
const year = Form.useWatch("year", form);
|
||||||
const quarter = Form.useWatch("quarter", form);
|
const semester = Form.useWatch("quarter", form);
|
||||||
|
const legEntIds = Form.useWatch("legal_entity_ids", form);
|
||||||
const isSubmitDisabled = !groupId || !subGroupId || !year || !quarter;
|
|
||||||
|
|
||||||
// Year options: current year and 3 years before
|
|
||||||
const yearOptions = useMemo(() => {
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
return Array.from({ length: 4 }, (_, i) => {
|
|
||||||
const y = currentYear - i;
|
|
||||||
return { value: y, label: String(y) };
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const quarterOptions = [
|
const quarterOptions = [
|
||||||
{ value: "Q1", label: "1 квартал" },
|
{ value: "Q1", label: "1 квартал" },
|
||||||
@ -51,36 +59,94 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
{ value: "Q3", label: "3 квартал" },
|
{ value: "Q3", label: "3 квартал" },
|
||||||
{ value: "Q4", label: "4 квартал" },
|
{ value: "Q4", label: "4 квартал" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Load groups if store is empty when modal opens
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && raw_rows.length === 0 && !loading) {
|
if (open && raw_rows.length === 0 && !loading) {
|
||||||
dispatch(getCompanyGroupsThunk());
|
dispatch(getCompanyGroupsThunk());
|
||||||
}
|
}
|
||||||
}, [open, raw_rows.length, loading, dispatch]);
|
}, [open, raw_rows.length, loading, dispatch]);
|
||||||
|
|
||||||
// When groupId changes -> reset second field, fetch subgroups locally
|
const isSubmitDisabled =
|
||||||
|
!company_group_id ||
|
||||||
|
!related_group_id ||
|
||||||
|
!year ||
|
||||||
|
!semester ||
|
||||||
|
!legEntIds?.length;
|
||||||
|
|
||||||
|
const yearOptions = useMemo(() => {
|
||||||
|
const y = new Date().getFullYear();
|
||||||
|
return Array.from({ length: 4 }, (_, i) => ({
|
||||||
|
value: y - i,
|
||||||
|
label: String(y - i),
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearLegEnts = () => {
|
||||||
|
setLegEntities([]);
|
||||||
|
setLoadingEntities(false);
|
||||||
|
setLegEntSearch("");
|
||||||
|
legEntReqRef.current++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleGroupChange = async (value?: string) => {
|
||||||
|
// form.setFieldsValue({
|
||||||
|
// related_group_id: undefined as any,
|
||||||
|
// legal_entity_ids: [],
|
||||||
|
// });
|
||||||
|
// setSubgroups([]);
|
||||||
|
// clearLegEnts();
|
||||||
|
// if (!value) return;
|
||||||
|
// const reqId = ++subgroupReqRef.current;
|
||||||
|
// setLoadingSubgroups(true);
|
||||||
|
// try {
|
||||||
|
// const data = await fetchGSKByComGroup(value);
|
||||||
|
// if (reqId === subgroupReqRef.current) setSubgroups(data);
|
||||||
|
// } finally {
|
||||||
|
// if (reqId === subgroupReqRef.current) setLoadingSubgroups(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleSubGroupChange = async (value?: string) => {
|
||||||
|
// form.setFieldsValue({ legal_entity_ids: [] });
|
||||||
|
// clearLegEnts();
|
||||||
|
// if (!value) return;
|
||||||
|
// const reqId = ++legEntReqRef.current;
|
||||||
|
// setLoadingEntities(true);
|
||||||
|
// try {
|
||||||
|
// const data = await fetchGroupMembers(value);
|
||||||
|
// if (reqId === legEntReqRef.current) setLegEntities(data);
|
||||||
|
// } finally {
|
||||||
|
// if (reqId === legEntReqRef.current) setLoadingEntities(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
const handleGroupChange = async (value?: string) => {
|
const handleGroupChange = async (value?: string) => {
|
||||||
form.setFieldsValue({ subGroupId: undefined as any });
|
form.setFieldsValue({
|
||||||
|
related_group_id: undefined as any,
|
||||||
|
legal_entity_ids: [],
|
||||||
|
});
|
||||||
setSubgroups([]);
|
setSubgroups([]);
|
||||||
|
clearLegEnts();
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
const reqId = ++subgroupReqRef.current;
|
||||||
const reqId = ++requestIdRef.current;
|
const leReqId = ++legEntReqRef.current;
|
||||||
setLoadingSubgroups(true);
|
setLoadingSubgroups(true);
|
||||||
|
setLoadingEntities(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchGSKByComGroup(value);
|
const dataSubgroups = await fetchGSKByComGroup(value);
|
||||||
// Ignore stale responses
|
const dataLegEnts = await fetchGroupMembers(value);
|
||||||
if (reqId === requestIdRef.current) setSubgroups(data);
|
if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups);
|
||||||
|
if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts);
|
||||||
} finally {
|
} finally {
|
||||||
if (reqId === requestIdRef.current) setLoadingSubgroups(false);
|
if (reqId === subgroupReqRef.current) setLoadingSubgroups(false);
|
||||||
|
if (leReqId === legEntReqRef.current) setLoadingEntities(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetLocal = () => {
|
const resetLocal = () => {
|
||||||
setSubgroups([]);
|
setSubgroups([]);
|
||||||
setLoadingSubgroups(false);
|
setLoadingSubgroups(false);
|
||||||
requestIdRef.current++; // invalidate any in-flight request
|
subgroupReqRef.current++;
|
||||||
|
clearLegEnts();
|
||||||
|
setLegEntSearch("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
@ -89,9 +155,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
await onOk(values);
|
await onOk(values);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
resetLocal();
|
resetLocal();
|
||||||
} catch {
|
} catch {}
|
||||||
// validation error - keep modal open
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@ -100,6 +164,12 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredLegEnts = useMemo(() => {
|
||||||
|
if (!legEntSearch) return legEntities;
|
||||||
|
const q = legEntSearch.toLowerCase();
|
||||||
|
return legEntities.filter((s) => s.name.toLowerCase().includes(q));
|
||||||
|
}, [legEntities, legEntSearch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Новая консолидация"
|
title="Новая консолидация"
|
||||||
@ -107,20 +177,25 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
okButtonProps={{ disabled: isSubmitDisabled }}
|
okButtonProps={{ disabled: isSubmitDisabled }}
|
||||||
okText="+ Создать"
|
okText="Submit"
|
||||||
cancelText="Отмена"
|
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
|
width={640}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
preserve={false}
|
||||||
|
initialValues={{ legEntIds: [] }}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" preserve={false}>
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="groupId"
|
name="company_group_id"
|
||||||
label="ГК"
|
label="Группа компаний"
|
||||||
rules={[{ required: true, message: "Укажите группу" }]}
|
rules={[{ required: true, message: "Укажите ГК" }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="Выберите группу"
|
placeholder="Выберите ГК"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
@ -131,20 +206,20 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
}
|
}
|
||||||
options={raw_rows.map((g) => ({ value: g.id, label: g.name }))}
|
options={raw_rows.map((g) => ({ value: g.id, label: g.name }))}
|
||||||
onChange={handleGroupChange}
|
onChange={handleGroupChange}
|
||||||
notFoundContent={loading ? "Загрузка" : "Нет вариантов"}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="subGroupId"
|
name="related_group_id"
|
||||||
label="ГСК/ГСЗ"
|
label="ГСК"
|
||||||
rules={[{ required: true, message: "Укажите ГСК" }]}
|
rules={[{ required: true, message: "Укажите ГСК" }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder={groupId ? "Выберите ГСК" : "Сначала выберите ГК"}
|
placeholder={
|
||||||
disabled={!groupId}
|
company_group_id ? "Выберите ГСК" : "Сначала выберите ГК"
|
||||||
|
}
|
||||||
|
disabled={!company_group_id}
|
||||||
loading={loadingSubgroups}
|
loading={loadingSubgroups}
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
@ -154,9 +229,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
.includes(input.toLowerCase())
|
.includes(input.toLowerCase())
|
||||||
}
|
}
|
||||||
options={subgroups.map((s) => ({ value: s.id, label: s.name }))}
|
options={subgroups.map((s) => ({ value: s.id, label: s.name }))}
|
||||||
notFoundContent={
|
// onChange={handleSubGroupChange}
|
||||||
loadingSubgroups ? "Загрузка" : "Нет вариантов"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@ -170,7 +243,6 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
<Select placeholder="Выберите год" options={yearOptions} />
|
<Select placeholder="Выберите год" options={yearOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="quarter"
|
name="quarter"
|
||||||
@ -180,6 +252,105 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
|
|||||||
<Select placeholder="Выберите квартал" options={quarterOptions} />
|
<Select placeholder="Выберите квартал" options={quarterOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{/* ROW 3 - full width block, only after subgroup is selected */}
|
||||||
|
{related_group_id && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.Item
|
||||||
|
name="legal_entity_ids"
|
||||||
|
label="ЮЛ консолидации"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "Выберите хотя бы одно ЮЛ" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Checkbox.Group style={{ width: "100%" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #d9d9d9",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Поиск ЮЛ..."
|
||||||
|
allowClear
|
||||||
|
value={legEntSearch}
|
||||||
|
onChange={(e) => setLegEntSearch(e.target.value)}
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{loadingEntities ? (
|
||||||
|
<div style={{ textAlign: "center", padding: 24 }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
) : filteredLegEnts.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={
|
||||||
|
legEntities.length === 0
|
||||||
|
? "No students"
|
||||||
|
: "No matches"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredLegEnts.map((s) => (
|
||||||
|
<Checkbox
|
||||||
|
key={s.id}
|
||||||
|
value={s.id}
|
||||||
|
style={{ width: "100%", display: "flex" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifySelf: "flex-start",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
gap: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ display: "inline-block" }}>
|
||||||
|
{s.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
color: "rgb(102, 112, 133)",
|
||||||
|
fontSize: 11,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.inn}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loadingEntities && legEntities.length > 0 && (
|
||||||
|
<div style={{ marginTop: 8, display: "flex", gap: 12 }}>
|
||||||
|
<span style={{ marginLeft: "auto", color: "#8c8c8c" }}>
|
||||||
|
Выбрано: {legEntIds?.length ?? 0} /{" "}
|
||||||
|
{legEntities.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import {
|
|||||||
import { USERS_ADMIN } from "../../auth/permissions";
|
import { USERS_ADMIN } from "../../auth/permissions";
|
||||||
import { useCurrentUser } from "../../auth/MeContext";
|
import { useCurrentUser } from "../../auth/MeContext";
|
||||||
import { MyModal } from "../NewConsoleModal/NewConsoleModal";
|
import { MyModal } from "../NewConsoleModal/NewConsoleModal";
|
||||||
|
import { api } from "../../api/client";
|
||||||
|
import { createConsolidation } from "../../services/api/consolidations";
|
||||||
|
|
||||||
type MenuItem = Required<MenuProps>["items"][number];
|
type MenuItem = Required<MenuProps>["items"][number];
|
||||||
|
|
||||||
@ -245,7 +247,7 @@ export function Sidebar() {
|
|||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
}}
|
}}
|
||||||
onOk={async (values) => {
|
onOk={async (values) => {
|
||||||
// await api.create(values)
|
await createConsolidation(values);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user