feat: add UL/IP auto-check in consolidation form, stub consolidation view, fix sidebar markup #5

Merged
maxim merged 1 commits from fix/autocheck into main 2026-10-08 10:39:15 +03:00
9 changed files with 314 additions and 37 deletions

View File

@ -10,6 +10,7 @@ import {
CatalogueGroup, CatalogueGroup,
CatalogueJur, CatalogueJur,
ConsolidationPage, ConsolidationPage,
ConsolidationView,
DocStorage, DocStorage,
GroupsLayout, GroupsLayout,
Main, Main,
@ -41,6 +42,10 @@ function App() {
<Route path="/files/:file_id" element={<FileViewer />} /> <Route path="/files/:file_id" element={<FileViewer />} />
<Route path="/docstorage" element={<DocStorage />} /> <Route path="/docstorage" element={<DocStorage />} />
<Route path="/logging" element={<AuditLogging />} /> <Route path="/logging" element={<AuditLogging />} />
<Route
path="/consolidations/:cons_id"
element={<ConsolidationView />}
/>
</Routes> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>

View File

@ -0,0 +1,97 @@
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useAppSelector, useAppDispatch } from "../store/store";
import PageTitle from "../components/PageTitle";
import { useEffect, useMemo, useState } from "react";
import { api } from "../api/client";
import { Button, Tag } from "antd";
import { TableTag } from "../components/TableTag/TableTag";
import { TAG_PRESETS } from "../components/TableTag/presets";
export function ConsolidationView() {
const [searchParams, setSearchParams] = useSearchParams();
const { cons_id } = useParams();
// const files = useAppSelector((state) => state.files.currentOpenFiles);
const navigate = useNavigate();
const [versionData, setVersionData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchVersionCard() {
if (!cons_id) return;
setLoading(true);
const res = await api.get(`/consolidations/versions/${cons_id}`);
const another_res = await api.get(
`/consolidations/versions/${cons_id}/osv`,
);
setVersionData(res);
setLoading(false);
}
fetchVersionCard();
}, [cons_id]);
const dispatch = useAppDispatch();
const desc = useMemo(() => {
return "";
}, []);
const headerContent = useMemo(() => {
if (!versionData) return { location: "", header: "" };
const year = versionData.year;
const quarter = versionData.quarter;
const company_name = versionData.company_group.name;
let header = `${company_name} - ${year} ${quarter}`;
let location = `Отчётность > ${company_name} > ${year} · ${quarter}`;
console.log({ header, location });
return { header, location };
}, [versionData]);
return (
<div
style={{
padding: 12,
display: "flex",
flexDirection: "column",
height: "100vh",
}}
>
{!loading && versionData !== null && (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
// alignItems: "center",
}}
>
<PageTitle {...headerContent} description={desc} backable />
<div
style={{
display: "flex",
gap: 10,
alignItems: "baseline",
fontSize: 12,
}}
>
<span style={{ color: "rgb(102, 112, 133)" }}>Статус отчета</span>
<TableTag
{...TAG_PRESETS.green}
style={{ fontSize: 11 }}
text={"Верифицирован"}
/>
<Button type="default" disabled size="small">
Изменить статус
</Button>
</div>
</div>
<main
style={{
flex: "1 1 auto",
minHeight: 0,
overflow: "auto",
backgroundColor: "white",
borderRadius: 12,
}}
></main>
</>
)}
</div>
);
}

View File

@ -1,3 +1,4 @@
export { ConsolidationView } from "./ConsolidationView";
export { Main } from "./Main"; export { Main } from "./Main";
export { CatalogueJur } from "./CatalogueJur"; export { CatalogueJur } from "./CatalogueJur";
export { ConsolidationPage } from "./ConsolidationsList"; export { ConsolidationPage } from "./ConsolidationsList";

View File

@ -21,21 +21,16 @@ export async function fetchGSKByComGroup(com_group_id: string) {
return res; return res;
} }
// export async function fetchGroupMembers(related_group_id: string) { export async function fetchGroupMembers(related_group_id: string) {
// const res = await api.get(`/directories/related-groups/${related_group_id}`); const res = await api.get(`/directories/related-groups/${related_group_id}`);
// const members = res?.members ?? []; const members = res?.members ?? [];
// console.log(members); return members;
// return members; }
// }
// legal-entities?company_group_id=<ГК>&closed=false export async function fetchLegEntsByComGroup(company_group_id: string) {
export async function fetchGroupMembers(company_group_id: string) {
const res = await api.get( const res = await api.get(
`/directories/legal-entities?company_group_id=${company_group_id}&closed=false`, `/directories/legal-entities?company_group_id=${company_group_id}&closed=false`,
); );
console.log(res);
const members = res?.items ?? []; const members = res?.items ?? [];
console.log(members);
return members; return members;
} }

View File

@ -169,7 +169,7 @@ export function ConsoleTableWidget() {
</Button> </Button>
<Button <Button
type="default" type="default"
onClick={() => navigate(`/cons/${row.con_id}/versions/${row.id}`)} onClick={() => navigate(`/consolidations/${row.id}`)}
> >
К отчетности <ArrowRightOutlined /> К отчетности <ArrowRightOutlined />
</Button> </Button>

View File

@ -0,0 +1,165 @@
import { Table, Tag } from "antd";
import {
CheckOutlined,
CloseOutlined,
QuestionOutlined,
} from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import { TableTag } from "../../components/TableTag/TableTag";
import { TAG_PRESETS } from "../../components/TableTag/presets";
import styles from "./styles.module.css";
type Status = "MATCH" | "MISMATCH" | "UNKNOWN";
type OverallStatus = "OK" | "ERROR" | "CHECK";
interface Row {
name: string;
check_status: Status;
file_status: Status;
overall_status: OverallStatus;
}
// Map a Status to its icon + color. Single source of truth.
const STATUS_CONFIG: Record<Status, { color: string; icon: React.ReactNode }> =
{
MATCH: { color: "#52c41a", icon: <CheckOutlined /> }, // green, 'v'
MISMATCH: { color: "#ff4d4f", icon: <CloseOutlined /> }, // red, '*'
UNKNOWN: { color: "#1677ff", icon: <QuestionOutlined /> }, // blue, '?'
};
const OVERALL_COLOR: Record<OverallStatus, string> = {
OK: TAG_PRESETS.green,
ERROR: TAG_PRESETS.red,
CHECK: TAG_PRESETS.orange,
};
// One stacked line: icon + label, colored by its own status.
function StatusLine({ label, status }: { label: string; status: Status }) {
const { color, icon } = STATUS_CONFIG[status];
return (
<span
style={{ color, display: "inline-flex", alignItems: "center", gap: 4 }}
>
{icon}
{label}
</span>
);
}
const columns: ColumnsType<Row> = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Overall",
dataIndex: "overall_status",
key: "overall_status",
render: (value: OverallStatus) => (
<TableTag {...OVERALL_COLOR[value]} text={value} />
),
},
{
title: "Statuses",
key: "simple_statuses",
// No dataIndex — we read the whole record from the second arg.
render: (_, record) => (
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<StatusLine label="Check" status={record.check_status} />
<StatusLine label="File" status={record.file_status} />
</div>
),
},
];
// Red background when any sub-status is MISMATCH or UNKNOWN.
function rowNeedsHighlight(row: Row): boolean {
const flagged: (Status | OverallStatus)[] = ["MISMATCH", "UNKNOWN", "ERROR"];
return (
flagged.includes(row.check_status) ||
flagged.includes(row.file_status) ||
flagged.includes(row.overall_status)
);
}
const mockData = [
{
key: "1",
name: "document_a.pdf",
check_status: "MATCH",
file_status: "MATCH",
overall_status: "OK",
},
{
key: "2",
name: "document_b.pdf",
check_status: "MISMATCH",
file_status: "MATCH",
overall_status: "ERROR",
},
{
key: "3",
name: "document_c.xlsx",
check_status: "MISMATCH",
file_status: "MISMATCH",
overall_status: "ERROR",
},
{
key: "4",
name: "document_d.docx",
check_status: "MATCH",
file_status: "MISMATCH",
overall_status: "ERROR",
},
{
key: "5",
name: "document_e.pdf",
check_status: "UNKNOWN",
file_status: "MATCH",
overall_status: "CHECK",
},
{
key: "6",
name: "document_f.pdf",
check_status: "MATCH",
file_status: "UNKNOWN",
overall_status: "CHECK",
},
{
key: "7",
name: "document_g.pdf",
check_status: "UNKNOWN",
file_status: "UNKNOWN",
overall_status: "CHECK",
},
{
key: "8",
name: "document_h.pdf",
check_status: "MISMATCH",
file_status: "UNKNOWN",
overall_status: "ERROR",
},
{
key: "9",
name: "document_i.pdf",
check_status: "UNKNOWN",
file_status: "MISMATCH",
overall_status: "ERROR",
},
];
export function FilesTable() {
return (
<Table<Row>
columns={columns}
dataSource={mockData}
pagination={false}
rowKey="name"
rowHoverable={false}
rowClassName={(record) =>
rowNeedsHighlight(record) ? styles.row_highlight : ""
}
/>
);
}

View File

@ -0,0 +1,8 @@
.row_highlight > td {
background-color: #fff1f0; /* soft red; use #ffccc7 for stronger */
}
/* Preserve the highlight on hover instead of Antd's default grey */
row_highlight:hover > td {
background-color: #ffe0dd !important;
}

View File

@ -11,8 +11,9 @@ import {
Spin, Spin,
} from "antd"; } from "antd";
import { import {
fetchGroupMembers, fetchLegEntsByComGroup,
fetchGSKByComGroup, fetchGSKByComGroup,
fetchGroupMembers,
} from "../../services/api/directories/company_groups"; } 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";
@ -46,6 +47,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
const legEntReqRef = useRef(0); const legEntReqRef = useRef(0);
const [legEntSearch, setLegEntSearch] = useState(""); const [legEntSearch, setLegEntSearch] = useState("");
const gskEntReqRef = useRef(0);
const company_group_id = Form.useWatch("company_group_id", form); const company_group_id = Form.useWatch("company_group_id", form);
const related_group_id = Form.useWatch("related_group_id", form); const related_group_id = Form.useWatch("related_group_id", form);
@ -105,19 +107,28 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
// } // }
// }; // };
// const handleSubGroupChange = async (value?: string) => { const handleSubGroupChange = async (value?: string) => {
// form.setFieldsValue({ legal_entity_ids: [] }); form.setFieldsValue({ legal_entity_ids: [] });
// clearLegEnts(); if (!value) return;
// if (!value) return; const reqId = ++gskEntReqRef.current;
// const reqId = ++legEntReqRef.current; setLoadingEntities(true);
// setLoadingEntities(true); try {
// try { const data = await fetchGroupMembers(value);
// const data = await fetchGroupMembers(value);
// if (reqId === legEntReqRef.current) setLegEntities(data); if (reqId !== gskEntReqRef.current) return;
// } finally { const gskEntitiesIds = new Set(data.map((c) => c.legal_entity_id));
// if (reqId === legEntReqRef.current) setLoadingEntities(false); const sorted = [...legEntities].sort((a, b) => {
// } const a_gsk = gskEntitiesIds.has(a.id);
// }; const b_gsk = gskEntitiesIds.has(b.id);
if (a_gsk !== b_gsk) return a_gsk ? -1 : 1;
return a.name.localeCompare(b.name);
});
setLegEntities(sorted);
form.setFieldsValue({ legal_entity_ids: [...gskEntitiesIds] });
} finally {
if (reqId === gskEntReqRef.current) setLoadingEntities(false);
}
};
const handleGroupChange = async (value?: string) => { const handleGroupChange = async (value?: string) => {
form.setFieldsValue({ form.setFieldsValue({
related_group_id: undefined as any, related_group_id: undefined as any,
@ -132,7 +143,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
setLoadingEntities(true); setLoadingEntities(true);
try { try {
const dataSubgroups = await fetchGSKByComGroup(value); const dataSubgroups = await fetchGSKByComGroup(value);
const dataLegEnts = await fetchGroupMembers(value); const dataLegEnts = await fetchLegEntsByComGroup(value);
if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups); if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups);
if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts); if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts);
} finally { } finally {
@ -181,12 +192,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
destroyOnHidden destroyOnHidden
width={640} width={640}
> >
<Form <Form form={form} layout="vertical" preserve={false}>
form={form}
layout="vertical"
preserve={false}
initialValues={{ legEntIds: [] }}
>
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
@ -229,7 +235,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 }))}
// onChange={handleSubGroupChange} onChange={handleSubGroupChange}
/> />
</Form.Item> </Form.Item>
</Col> </Col>
@ -289,8 +295,8 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
image={Empty.PRESENTED_IMAGE_SIMPLE} image={Empty.PRESENTED_IMAGE_SIMPLE}
description={ description={
legEntities.length === 0 legEntities.length === 0
? "No students" ? "Нет строк"
: "No matches" : "Нет совпадений"
} }
/> />
) : ( ) : (

View File

@ -33,7 +33,6 @@
.section { .section {
margin-bottom: 0px; margin-bottom: 0px;
padding: 12px;
border-radius: 10px; border-radius: 10px;
} }
@ -75,6 +74,7 @@
.linkBase { .linkBase {
display: inline-flex; display: inline-flex;
font-size: 13px;
gap: 10px; gap: 10px;
padding: 8px 10px; padding: 8px 10px;
width: 100%; width: 100%;