diff --git a/src/components/GenericTable/DataTable.tsx b/src/components/GenericTable/DataTable.tsx index 90b05a0..f95d984 100644 --- a/src/components/GenericTable/DataTable.tsx +++ b/src/components/GenericTable/DataTable.tsx @@ -33,6 +33,9 @@ interface DataTableProps> { scrollY?: number | string; onRow?: TableProps["onRow"]; } +const locale = { + emptyText: "Нет данных, чтобы отобразить", +}; export function DataTable>({ config, @@ -209,6 +212,7 @@ export function DataTable>({ return (
+ locale={locale} rowKey={rowKey} loading={loading} columns={columns} diff --git a/src/components/TableTag/TableTag.tsx b/src/components/TableTag/TableTag.tsx new file mode 100644 index 0000000..ca8d196 --- /dev/null +++ b/src/components/TableTag/TableTag.tsx @@ -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 = ({ + text, + color = "#1677ff", + bgColor = "#e6f4ff", + borderColor = "#91caff", + icon, + rounded = true, + style, + className, +}) => { + return ( + + {icon && {icon}} + {text} + + ); +}; diff --git a/src/components/TableTag/presets.tsx b/src/components/TableTag/presets.tsx new file mode 100644 index 0000000..cf66b8d --- /dev/null +++ b/src/components/TableTag/presets.tsx @@ -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; diff --git a/src/layouts/ConsolidationsList.tsx b/src/layouts/ConsolidationsList.tsx index 2419176..cf9b262 100644 --- a/src/layouts/ConsolidationsList.tsx +++ b/src/layouts/ConsolidationsList.tsx @@ -16,30 +16,8 @@ import PageTitle from "../components/PageTitle"; import { fetchOneCompanyGroup } from "../services/api/directories/company_groups"; import { getConsolidationsListThunk } from "../store/slices/consolidations/thunks"; import { ConsoleTableWidget } from "../widgets/ConsoleTable/ConsoleTable"; - -function DrawerContent() { - 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 ( - <> -
- Version_id: {current_version_id}, legal_entity_id:{" "} - {current_legal_entity_id} -
- {}} - /> - {drawerContent.map((f) => ( - - ))} - - ); -} +import { setSelectedVersion } from "../store/slices/consolidations/consolidationsSlice"; +import { DocumentsDrawer } from "../widgets/FilesDrawer/FilesDrawer"; export function ConsolidationPage() { const [legalEntities, setLegalEntities] = useState([]); @@ -65,6 +43,9 @@ export function ConsolidationPage() { // dispatch() } getAllInfo(); + return () => { + dispatch(setSelectedVersion(null)); + }; }, [dispatch, group_id]); return (
- { setDrawerOpen(false); }} - > - - + />
); } diff --git a/src/layouts/FileViewer.tsx b/src/layouts/FileViewer.tsx index ac41650..c7548c9 100644 --- a/src/layouts/FileViewer.tsx +++ b/src/layouts/FileViewer.tsx @@ -10,7 +10,7 @@ import { Select } from "antd"; import PdfDataViewer from "../widgets/PDFResultViewer/PDFDataViewer"; import { Config } from "../conf/config"; import IFrameBlock from "../widgets/PDFResultViewer/IFrameBlock"; -import { getFilesThunk } from "../store/slices/fileSlice"; +import { getFilesThunk } from "../store/slices/files/thunks"; function PdfViewer({ fileId }: { fileId: string }) { return ( diff --git a/src/services/api/consolidations.ts b/src/services/api/consolidations.ts index c2390a8..b3c76d0 100644 --- a/src/services/api/consolidations.ts +++ b/src/services/api/consolidations.ts @@ -2,14 +2,28 @@ import { api } from "../../api/client"; import { MOCK_CONS, versionCardsMock } from "./mocks"; export async function getConsolidations(company_group_id: string) { - // const res = await api.get( - // `/company-groups/${company_group_id}/consolidations`, - // ); + const res = await api.get( + `/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) { - 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; } diff --git a/src/services/api/directories/company_groups.ts b/src/services/api/directories/company_groups.ts index 5eecc95..6cf1837 100644 --- a/src/services/api/directories/company_groups.ts +++ b/src/services/api/directories/company_groups.ts @@ -20,3 +20,22 @@ export async function fetchGSKByComGroup(com_group_id: string) { ); 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; +} diff --git a/src/services/api/files.ts b/src/services/api/files.ts new file mode 100644 index 0000000..80d312f --- /dev/null +++ b/src/services/api/files.ts @@ -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; +} diff --git a/src/store/slices/fileSlice.ts b/src/store/slices/fileSlice.ts deleted file mode 100644 index bcf8c7e..0000000 --- a/src/store/slices/fileSlice.ts +++ /dev/null @@ -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; diff --git a/src/store/slices/files/fileSlice.ts b/src/store/slices/files/fileSlice.ts new file mode 100644 index 0000000..9c86f74 --- /dev/null +++ b/src/store/slices/files/fileSlice.ts @@ -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; diff --git a/src/store/slices/files/thunks.ts b/src/store/slices/files/thunks.ts new file mode 100644 index 0000000..93ae576 --- /dev/null +++ b/src/store/slices/files/thunks.ts @@ -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); + }, +); diff --git a/src/store/store.ts b/src/store/store.ts index 08c374e..00f238f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,5 +1,5 @@ import { configureStore } from "@reduxjs/toolkit"; -import filesReducer from "./slices/fileSlice"; +import filesReducer from "./slices/files/fileSlice"; import comGroupsReducer from "./slices/companyGroupsSlice/companyGroupsSlice"; import consolidationsReducer from "./slices/consolidations/consolidationsSlice"; import { useDispatch, useSelector } from "react-redux"; diff --git a/src/types/consolidations/consolidations.ts b/src/types/consolidations/consolidations.ts index fe31dc2..b23c0e1 100644 --- a/src/types/consolidations/consolidations.ts +++ b/src/types/consolidations/consolidations.ts @@ -19,7 +19,7 @@ interface IConsolidationVersion { } export interface IConsolidation { - id: string; + consolidation_id: string; year: number; quarter: QuarterEnum; reporting_date: string; diff --git a/src/types/consolidations/version_members.ts b/src/types/consolidations/version_members.ts index b454516..70be1e5 100644 --- a/src/types/consolidations/version_members.ts +++ b/src/types/consolidations/version_members.ts @@ -28,7 +28,14 @@ type VersionHistory = { comment: string | null; }; -type FileStatus = +export type DocumentType = + | "BALANCE" + | "INCOME" + | "OSV" + | "DISCLOSURE" + | "OTHER"; + +export type FileStatus = | "CONFIRMED" | "NEEDS_CHECK" | "RECOGNIZED" @@ -36,10 +43,20 @@ type FileStatus = | "ERROR" | "MISSING"; +export type FileRussianStatus = + | "Требуется проверка" + | "Ошибка" + | "Отсутствует" + | "Загружен" + | "В обработке" + | "Распознан"; + type ChecklistItem = { document_ids: string[]; + document_type: DocumentType; status: FileStatus; missing_quarters: QuarterEnum[]; + account?: string | null; }; type VersionMember = { @@ -48,6 +65,7 @@ type VersionMember = { inn: string; is_head_entity: string; checklist: ChecklistItem[]; + entity_type: "ИП" | "ЮЛ"; }; export interface IVersionCard { @@ -78,4 +96,8 @@ export type VersionCardRow = { inn: string; name: string; version_id: string; + bb_status: FileRussianStatus; + uploaded_osvs: string; + ofr_status: FileRussianStatus; + entity_type: "ЮЛ" | "ИП"; }; diff --git a/src/widgets/CompanyGroupsTable/config.tsx b/src/widgets/CompanyGroupsTable/config.tsx index f8c983f..a69f729 100644 --- a/src/widgets/CompanyGroupsTable/config.tsx +++ b/src/widgets/CompanyGroupsTable/config.tsx @@ -1,4 +1,3 @@ -import { Tag } from "antd"; import type { TableConfig } from "../../components/GenericTable"; import { ICompanyGroup } from "../../types/companyGroups/companyGroups"; diff --git a/src/widgets/ConsoleLegalEntities/config.tsx b/src/widgets/ConsoleLegalEntities/config.tsx index ef85f45..60c5f32 100644 --- a/src/widgets/ConsoleLegalEntities/config.tsx +++ b/src/widgets/ConsoleLegalEntities/config.tsx @@ -5,6 +5,9 @@ import { VersionCardRow, } from "../../types/consolidations/version_members"; 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< VersionCardRow & { docs: "Документы" } @@ -27,6 +30,27 @@ export const membersConfig: TableConfig< ), }, + { + key: "ofr_status", + title: "ОФР", + features: { multiselect: true }, + render: (v) => , + }, + + { + key: "bb_status", + title: "Бух.баланс", + features: { multiselect: true }, + render: (v) => , + }, + { + key: "uploaded_osvs", + title: "ОСВ", + features: { multiselect: true }, + render: (v) => , + }, { key: "inn", title: "ИНН", features: { search: true, multiselect: true } }, + + { key: "entity_type", title: "Тип", features: { multiselect: true } }, ], }; diff --git a/src/widgets/ConsoleLegalEntities/consts.ts b/src/widgets/ConsoleLegalEntities/consts.ts new file mode 100644 index 0000000..f8960d0 --- /dev/null +++ b/src/widgets/ConsoleLegalEntities/consts.ts @@ -0,0 +1,23 @@ +import { Preset, TAG_PRESETS } from "../../components/TableTag/presets"; +import { + FileRussianStatus, + FileStatus, +} from "../../types/consolidations/version_members"; + +export const statusMapping: Record = { + CONFIRMED: "Загружен", + ERROR: "Ошибка", + MISSING: "Отсутствует", + NEEDS_CHECK: "Требуется проверка", + PROCESSING: "В обработке", + RECOGNIZED: "Распознан", +}; + +export const statusColorMapping: Record = { + "В обработке": TAG_PRESETS.geekblue, + Загружен: TAG_PRESETS.green, + Отсутствует: TAG_PRESETS.red, + Ошибка: TAG_PRESETS.red, + Распознан: TAG_PRESETS.gold, + "Требуется проверка": TAG_PRESETS.orange, +}; diff --git a/src/widgets/ConsoleLegalEntities/normalize.ts b/src/widgets/ConsoleLegalEntities/normalize.ts index 9688cde..32b119b 100644 --- a/src/widgets/ConsoleLegalEntities/normalize.ts +++ b/src/widgets/ConsoleLegalEntities/normalize.ts @@ -2,6 +2,7 @@ import { IVersionCard, VersionCardRow, } from "../../types/consolidations/version_members"; +import { statusMapping } from "./consts"; export function normalizeData( cons_data: IVersionCard | null, @@ -10,13 +11,30 @@ export function normalizeData( if (cons_data === null) { return result; } + console.log(cons_data); for (const m of cons_data.members) { - result.push({ + let row: VersionCardRow = { id: m.id, name: m.name, inn: m.inn, 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; } diff --git a/src/widgets/ConsoleTable/ConsoleTable.tsx b/src/widgets/ConsoleTable/ConsoleTable.tsx index f4f8e43..ff911da 100644 --- a/src/widgets/ConsoleTable/ConsoleTable.tsx +++ b/src/widgets/ConsoleTable/ConsoleTable.tsx @@ -169,9 +169,7 @@ export function ConsoleTableWidget() { diff --git a/src/widgets/ConsoleTable/config.tsx b/src/widgets/ConsoleTable/config.tsx index ddfa5d8..f70202a 100644 --- a/src/widgets/ConsoleTable/config.tsx +++ b/src/widgets/ConsoleTable/config.tsx @@ -1,16 +1,8 @@ -import { Tag } from "antd"; import { TableConfig } from "../../components/GenericTable"; import { ConsRow, LockOut } from "../../types/consolidations/consolidations"; import { statusColorMapping } from "./consts"; - -const statusColor: Record = { - INITIATED: "gold", - DOCUMENTS_PROCESSING: "blue", - VERIFIED: "green", - CANCELLED: "default", - REJECTED: "red", - ARCHIVED: "default", -}; +import { TableTag } from "../../components/TableTag/TableTag"; +import { TAG_PRESETS } from "../../components/TableTag/presets"; export const consConfigBase: TableConfig = { rowKey: "id", @@ -24,11 +16,7 @@ export const consConfigBase: TableConfig = { key: "status", title: "Статус", features: { multiselect: true, sort: true }, - render: (v) => ( - - {v} - - ), + render: (v) => , }, { key: "reporting_date", diff --git a/src/widgets/ConsoleTable/consts.ts b/src/widgets/ConsoleTable/consts.ts index f1db3dc..e1b98a4 100644 --- a/src/widgets/ConsoleTable/consts.ts +++ b/src/widgets/ConsoleTable/consts.ts @@ -1,3 +1,4 @@ +import { TAG_PRESETS, type Preset } from "../../components/TableTag/presets"; import { ConsRussianStatus, ConsStatus, @@ -18,17 +19,17 @@ export const statusMapping: Record = { REJECTED: "Отклонён", }; -export const statusColorMapping: Record = { - "В архиве": "default", - "Верификация ГО": "geekblue", - "Верификация РФ": "geekblue", - Верифицирован: "green", - Инициация: "default", - "Консолидация ГО": "purple", - "Консолидация РФ": "purple", - Корректировка: "gold", - "Коррекция данных": "gold", - "Обработка документов": "gold", - Отклонён: "red", - Отменён: "red", +export const statusColorMapping: Record = { + "В архиве": TAG_PRESETS.default, + "Верификация ГО": TAG_PRESETS.geekblue, + "Верификация РФ": TAG_PRESETS.geekblue, + Верифицирован: TAG_PRESETS.green, + Инициация: TAG_PRESETS.default, + "Консолидация ГО": TAG_PRESETS.purple, + "Консолидация РФ": TAG_PRESETS.purple, + Корректировка: TAG_PRESETS.gold, + "Коррекция данных": TAG_PRESETS.gold, + "Обработка документов": TAG_PRESETS.gold, + Отклонён: TAG_PRESETS.red, + Отменён: TAG_PRESETS.red, }; diff --git a/src/widgets/ConsoleTable/flatten.ts b/src/widgets/ConsoleTable/flatten.ts index e04abbf..a5a2c89 100644 --- a/src/widgets/ConsoleTable/flatten.ts +++ b/src/widgets/ConsoleTable/flatten.ts @@ -16,11 +16,11 @@ export function flattenCons(cons: IConsolidation[]): ConsRow[] { let rep_date = new Date(con.reporting_date) ?? null; out.push({ id: v.id, - group_id: con.id, + group_id: con.consolidation_id, isPrimary: i === 0, versionIndex: i, groupSize: con.versions.length, - con_id: con.id, + con_id: con.consolidation_id, year: con.year, quarter: con.quarter, reporting_date: rep_date, diff --git a/src/widgets/FilesDrawer/FilesDrawer.tsx b/src/widgets/FilesDrawer/FilesDrawer.tsx new file mode 100644 index 0000000..7418b31 --- /dev/null +++ b/src/widgets/FilesDrawer/FilesDrawer.tsx @@ -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 ( + setUploadOpen(true)}> + Upload documents + + } + > + ( + + {item.original_name} - {item.file_type} - {item.status} + + )} + /> + + {current_legal_entity_id && ( + setUploadOpen(false)} + onUploaded={() => dispatch(getFilesThunk(current_legal_entity_id))} // instant refresh, polling covers steady state + /> + )} + + ); +} diff --git a/src/widgets/FilesDrawer/UploadDocumentsModal.tsx b/src/widgets/FilesDrawer/UploadDocumentsModal.tsx new file mode 100644 index 0000000..20eb32c --- /dev/null +++ b/src/widgets/FilesDrawer/UploadDocumentsModal.tsx @@ -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([]); + 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 ( + + { + // 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 }} + > +

+ +

+

Выберите файлы

+
+
+ ); +} diff --git a/src/widgets/FilesDrawer/useDocumentsPolling.ts b/src/widgets/FilesDrawer/useDocumentsPolling.ts new file mode 100644 index 0000000..2abdc6b --- /dev/null +++ b/src/widgets/FilesDrawer/useDocumentsPolling.ts @@ -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 }; +} diff --git a/src/widgets/NewConsoleModal/NewConsoleModal.tsx b/src/widgets/NewConsoleModal/NewConsoleModal.tsx index 656dc6b..6dd76b2 100644 --- a/src/widgets/NewConsoleModal/NewConsoleModal.tsx +++ b/src/widgets/NewConsoleModal/NewConsoleModal.tsx @@ -1,14 +1,28 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; -import { Modal, Form, Select, Row, Col } from "antd"; -import { fetchGSKByComGroup } from "../../services/api/directories/company_groups"; +import { + 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 { getCompanyGroupsThunk } from "../../store/slices/companyGroupsSlice/thunks"; type FormValues = { - groupId: string; - subGroupId: string; + company_group_id: string; + related_group_id: string; year: number; quarter: string; // example of 2nd static field + legal_entity_ids: string[]; }; type Props = { @@ -23,27 +37,21 @@ export const MyModal: React.FC = ({ open, onCancel, onOk }) => { const { raw_rows, loading } = useAppSelector((state) => state.company_groups); - // Subgroups are local — only this modal needs them - const [subgroups, setSubgroups] = useState([]); + const [subgroups, setSubgroups] = useState([]); const [loadingSubgroups, setLoadingSubgroups] = useState(false); - // Guards against out-of-order responses when group is changed quickly - const requestIdRef = useRef(0); + const subgroupReqRef = useRef(0); - const groupId = Form.useWatch("groupId", form); - const subGroupId = Form.useWatch("subGroupId", form); + const [legEntities, setLegEntities] = useState([]); + 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 quarter = Form.useWatch("quarter", 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 semester = Form.useWatch("quarter", form); + const legEntIds = Form.useWatch("legal_entity_ids", form); const quarterOptions = [ { value: "Q1", label: "1 квартал" }, @@ -51,36 +59,94 @@ export const MyModal: React.FC = ({ open, onCancel, onOk }) => { { value: "Q3", label: "3 квартал" }, { value: "Q4", label: "4 квартал" }, ]; - - // Load groups if store is empty when modal opens useEffect(() => { if (open && raw_rows.length === 0 && !loading) { dispatch(getCompanyGroupsThunk()); } }, [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) => { - form.setFieldsValue({ subGroupId: undefined as any }); + form.setFieldsValue({ + related_group_id: undefined as any, + legal_entity_ids: [], + }); setSubgroups([]); - + clearLegEnts(); if (!value) return; - - const reqId = ++requestIdRef.current; + const reqId = ++subgroupReqRef.current; + const leReqId = ++legEntReqRef.current; setLoadingSubgroups(true); + setLoadingEntities(true); try { - const data = await fetchGSKByComGroup(value); - // Ignore stale responses - if (reqId === requestIdRef.current) setSubgroups(data); + const dataSubgroups = await fetchGSKByComGroup(value); + const dataLegEnts = await fetchGroupMembers(value); + if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups); + if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts); } finally { - if (reqId === requestIdRef.current) setLoadingSubgroups(false); + if (reqId === subgroupReqRef.current) setLoadingSubgroups(false); + if (leReqId === legEntReqRef.current) setLoadingEntities(false); } }; const resetLocal = () => { setSubgroups([]); setLoadingSubgroups(false); - requestIdRef.current++; // invalidate any in-flight request + subgroupReqRef.current++; + clearLegEnts(); + setLegEntSearch(""); }; const handleOk = async () => { @@ -89,9 +155,7 @@ export const MyModal: React.FC = ({ open, onCancel, onOk }) => { await onOk(values); form.resetFields(); resetLocal(); - } catch { - // validation error - keep modal open - } + } catch {} }; const handleCancel = () => { @@ -100,6 +164,12 @@ export const MyModal: React.FC = ({ open, onCancel, onOk }) => { onCancel(); }; + const filteredLegEnts = useMemo(() => { + if (!legEntSearch) return legEntities; + const q = legEntSearch.toLowerCase(); + return legEntities.filter((s) => s.name.toLowerCase().includes(q)); + }, [legEntities, legEntSearch]); + return ( = ({ open, onCancel, onOk }) => { onCancel={handleCancel} onOk={handleOk} okButtonProps={{ disabled: isSubmitDisabled }} - okText="+ Создать" - cancelText="Отмена" + okText="Submit" destroyOnHidden + width={640} > -
+ = ({ open, onCancel, onOk }) => { .includes(input.toLowerCase()) } options={subgroups.map((s) => ({ value: s.id, label: s.name }))} - notFoundContent={ - loadingSubgroups ? "Загрузка" : "Нет вариантов" - } + // onChange={handleSubGroupChange} /> @@ -170,7 +243,6 @@ export const MyModal: React.FC = ({ open, onCancel, onOk }) => { + + {/* ROW 3 - full width block, only after subgroup is selected */} + {related_group_id && ( + + + +
+ setLegEntSearch(e.target.value)} + style={{ marginBottom: 12 }} + /> + + {loadingEntities ? ( +
+ +
+ ) : filteredLegEnts.length === 0 ? ( + + ) : ( +
+ {filteredLegEnts.map((s) => ( + +
+ + {s.name} + + + {s.inn} + +
+
+ ))} +
+ )} + + {!loadingEntities && legEntities.length > 0 && ( +
+ + Выбрано: {legEntIds?.length ?? 0} /{" "} + {legEntities.length} + +
+ )} +
+
+
+ + )}
diff --git a/src/widgets/Sidebar/Sidebar.tsx b/src/widgets/Sidebar/Sidebar.tsx index fadf8d6..26d700f 100644 --- a/src/widgets/Sidebar/Sidebar.tsx +++ b/src/widgets/Sidebar/Sidebar.tsx @@ -24,6 +24,8 @@ import { import { USERS_ADMIN } from "../../auth/permissions"; import { useCurrentUser } from "../../auth/MeContext"; import { MyModal } from "../NewConsoleModal/NewConsoleModal"; +import { api } from "../../api/client"; +import { createConsolidation } from "../../services/api/consolidations"; type MenuItem = Required["items"][number]; @@ -245,7 +247,7 @@ export function Sidebar() { setModalOpen(false); }} onOk={async (values) => { - // await api.create(values) + await createConsolidation(values); setModalOpen(false); }} />