feat: add UL/IP auto-check in consolidation form, stub consolidation view, fix sidebar markup #5
@ -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>
|
||||||
|
|||||||
97
src/layouts/ConsolidationView.tsx
Normal file
97
src/layouts/ConsolidationView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
165
src/widgets/FilesDrawer/FilesTable.tsx
Normal file
165
src/widgets/FilesDrawer/FilesTable.tsx
Normal 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 : ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/widgets/FilesDrawer/styles.module.css
Normal file
8
src/widgets/FilesDrawer/styles.module.css
Normal 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;
|
||||||
|
}
|
||||||
@ -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"
|
: "Нет совпадений"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user