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

This commit is contained in:
maxim 2026-10-08 05:00:11 +03:00
parent 8747f6318c
commit 85f654406a
9 changed files with 314 additions and 37 deletions

View File

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

View File

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

View File

@ -169,7 +169,7 @@ export function ConsoleTableWidget() {
</Button>
<Button
type="default"
onClick={() => navigate(`/cons/${row.con_id}/versions/${row.id}`)}
onClick={() => navigate(`/consolidations/${row.id}`)}
>
К отчетности <ArrowRightOutlined />
</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,
} from "antd";
import {
fetchGroupMembers,
fetchLegEntsByComGroup,
fetchGSKByComGroup,
fetchGroupMembers,
} from "../../services/api/directories/company_groups";
import { useAppDispatch, useAppSelector } from "../../store/store";
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 [legEntSearch, setLegEntSearch] = useState("");
const gskEntReqRef = useRef(0);
const company_group_id = Form.useWatch("company_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) => {
// 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 handleSubGroupChange = async (value?: string) => {
form.setFieldsValue({ legal_entity_ids: [] });
if (!value) return;
const reqId = ++gskEntReqRef.current;
setLoadingEntities(true);
try {
const data = await fetchGroupMembers(value);
if (reqId !== gskEntReqRef.current) return;
const gskEntitiesIds = new Set(data.map((c) => c.legal_entity_id));
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) => {
form.setFieldsValue({
related_group_id: undefined as any,
@ -132,7 +143,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
setLoadingEntities(true);
try {
const dataSubgroups = await fetchGSKByComGroup(value);
const dataLegEnts = await fetchGroupMembers(value);
const dataLegEnts = await fetchLegEntsByComGroup(value);
if (reqId === subgroupReqRef.current) setSubgroups(dataSubgroups);
if (leReqId === legEntReqRef.current) setLegEntities(dataLegEnts);
} finally {
@ -181,12 +192,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
destroyOnHidden
width={640}
>
<Form
form={form}
layout="vertical"
preserve={false}
initialValues={{ legEntIds: [] }}
>
<Form form={form} layout="vertical" preserve={false}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
@ -229,7 +235,7 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
.includes(input.toLowerCase())
}
options={subgroups.map((s) => ({ value: s.id, label: s.name }))}
// onChange={handleSubGroupChange}
onChange={handleSubGroupChange}
/>
</Form.Item>
</Col>
@ -289,8 +295,8 @@ export const MyModal: React.FC<Props> = ({ open, onCancel, onOk }) => {
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
legEntities.length === 0
? "No students"
: "No matches"
? "Нет строк"
: "Нет совпадений"
}
/>
) : (

View File

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