Compare commits
44 Commits
5ddf7479a4
...
5eb73ddb37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb73ddb37 | ||
|
|
4d5a39ef12 | ||
|
|
ccba554b86 | ||
| c4dd35b6bd | |||
| ffe4ff9edb | |||
| 0a99aeed0c | |||
| 8a280781bb | |||
|
|
88e12d1a8b | ||
|
|
6f0b3eb0cc | ||
|
|
97934a3404 | ||
|
|
0b33ba2b40 | ||
|
|
bc4e5122c6 | ||
|
|
acd4645a4e | ||
| bdb4c389ce | |||
| 5e6ad47128 | |||
| 19dd443146 | |||
|
|
2cd80343ab | ||
|
|
c0e186baef | ||
|
|
1df748fb03 | ||
|
|
2cc53b6846 | ||
|
|
944fbe4d7e | ||
| 155888f7fc | |||
|
|
019b93092c | ||
|
|
a23a2dffc6 | ||
| 2e781ca809 | |||
|
|
cc62551c92 | ||
|
|
40c373b5ed | ||
| 4ec4936a0f | |||
| 88a9713fb3 | |||
|
|
4e7cc9b3bd | ||
|
|
c8cc1afec1 | ||
| 047cabe14b | |||
| 7fabb22c45 | |||
| 66846c6afa | |||
| b283bda007 | |||
|
|
4f8393bedc | ||
| 796640a491 | |||
| e91c5e1846 | |||
| c1059736fb | |||
| 7a3ef892cf | |||
| 2e5cf3d2af | |||
| 62a2f9bae4 | |||
|
|
e28f6a3887 | ||
| 9c725e9678 |
@ -762,6 +762,43 @@ def upgrade() -> None:
|
|||||||
['code'],
|
['code'],
|
||||||
schema='v3',
|
schema='v3',
|
||||||
)
|
)
|
||||||
|
create_table_if_not_exists(
|
||||||
|
'form3_phase',
|
||||||
|
sa.Column('rf_project_report_id', sa.Integer(),primary_key=True),
|
||||||
|
sa.Column('phase_code', sa.String(), primary_key=True),
|
||||||
|
sa.Column('role', sa.String(), nullable=False),
|
||||||
|
sa.Column('column_keys', postgresql.ARRAY(sa.Text()), nullable=False),
|
||||||
|
sa.Column('opens_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('closes_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_window', 'form3_phase',
|
||||||
|
sa.text('closes_at > opens_at'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_columns', 'form3_phase',
|
||||||
|
sa.text('cardinality(column_keys) > 0'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_no_admin', 'form3_phase',
|
||||||
|
sa.text("role != 'ADMIN'"),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_active', 'form3_phase',
|
||||||
|
['rf_project_report_id', 'role', 'opens_at', 'closes_at'],
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_columns_gin', 'form3_phase',
|
||||||
|
['column_keys'],
|
||||||
|
schema='v3',
|
||||||
|
postgresql_using='gin',
|
||||||
|
)
|
||||||
create_table_if_not_exists(
|
create_table_if_not_exists(
|
||||||
'security_detail',
|
'security_detail',
|
||||||
sa.Column('id', sa.Integer(),
|
sa.Column('id', sa.Integer(),
|
||||||
|
|||||||
63
api/alembic/versions/0004_vsp_delete_with_close_date.py
Normal file
63
api/alembic/versions/0004_vsp_delete_with_close_date.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0004"
|
||||||
|
down_revision = "0003"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.del_vsp(p_vsp_id integer, p_deleted_by integer)
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old RECORD;
|
||||||
|
v_usr RECORD;
|
||||||
|
BEGIN
|
||||||
|
SELECT vsp.id, vsp.branch_id
|
||||||
|
INTO v_old
|
||||||
|
FROM v3.vsp vsp WHERE vsp.id = p_vsp_id;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
SELECT usr.id
|
||||||
|
INTO v_usr
|
||||||
|
FROM v3.app_user usr WHERE usr.id = p_deleted_by;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'user #% не существует', p_deleted_by;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE v3.vsp
|
||||||
|
SET is_deleted = true,
|
||||||
|
is_active = false,
|
||||||
|
updated_by = p_deleted_by
|
||||||
|
WHERE id = p_vsp_id;
|
||||||
|
UPDATE v3.vsp
|
||||||
|
SET closed_at = CURRENT_DATE
|
||||||
|
WHERE id = p_vsp_id
|
||||||
|
AND closed_at IS NULL;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'DELETE_VSP', 'VSP',
|
||||||
|
jsonb_build_object(
|
||||||
|
'vsp_id', p_vsp_id,
|
||||||
|
'entity_id', p_vsp_id,
|
||||||
|
'branch_id', v_old.branch_id,
|
||||||
|
'updated_by', p_deleted_by
|
||||||
|
),
|
||||||
|
null, null, v_old.branch_id
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN p_vsp_id;
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@ -4078,26 +4078,12 @@ $function$
|
|||||||
|
|
||||||
-- DROP FUNCTION v3.v_form1_smeta(int4, int4);
|
-- DROP FUNCTION v3.v_form1_smeta(int4, int4);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id integer)
|
||||||
CREATE FUNCTION v3.v_form1_smeta(p_year INT, p_org_unit_id INT)
|
RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, supp_plan_q1 numeric, supp_plan_q2 numeric, supp_plan_q3 numeric, supp_plan_q4 numeric, supp_plan_year numeric, dev_plan_q1 numeric, dev_plan_q2 numeric, dev_plan_q3 numeric, dev_plan_q4 numeric, dev_plan_year numeric, total_plan_year numeric, supp_appr_q1 numeric, supp_appr_q2 numeric, supp_appr_q3 numeric, supp_appr_q4 numeric, supp_appr_year numeric, dev_appr_q1 numeric, dev_appr_q2 numeric, dev_appr_q3 numeric, dev_appr_q4 numeric, dev_appr_year numeric, total_appr_year numeric, supp_act_q1 numeric, supp_act_q2 numeric, supp_act_q3 numeric, supp_act_q4 numeric, supp_act_year numeric, dev_act_q1 numeric, dev_act_q2 numeric, dev_act_q3 numeric, dev_act_q4 numeric, dev_act_year numeric, total_act_year numeric, supp_corr_q2 numeric, supp_corr_q3 numeric, supp_corr_q4 numeric, dev_corr_q2 numeric, dev_corr_q3 numeric, dev_corr_q4 numeric)
|
||||||
RETURNS TABLE (
|
LANGUAGE plpgsql
|
||||||
row_type VARCHAR,
|
STABLE
|
||||||
depth INT,
|
SET search_path TO 'v3', 'pg_catalog'
|
||||||
section_code VARCHAR,
|
AS $function$
|
||||||
name VARCHAR,
|
|
||||||
supp_plan_q1 NUMERIC, supp_plan_q2 NUMERIC, supp_plan_q3 NUMERIC, supp_plan_q4 NUMERIC, supp_plan_year NUMERIC,
|
|
||||||
dev_plan_q1 NUMERIC, dev_plan_q2 NUMERIC, dev_plan_q3 NUMERIC, dev_plan_q4 NUMERIC, dev_plan_year NUMERIC,
|
|
||||||
total_plan_year NUMERIC,
|
|
||||||
supp_appr_q1 NUMERIC, supp_appr_q2 NUMERIC, supp_appr_q3 NUMERIC, supp_appr_q4 NUMERIC, supp_appr_year NUMERIC,
|
|
||||||
dev_appr_q1 NUMERIC, dev_appr_q2 NUMERIC, dev_appr_q3 NUMERIC, dev_appr_q4 NUMERIC, dev_appr_year NUMERIC,
|
|
||||||
total_appr_year NUMERIC,
|
|
||||||
supp_act_q1 NUMERIC, supp_act_q2 NUMERIC, supp_act_q3 NUMERIC, supp_act_q4 NUMERIC, supp_act_year NUMERIC,
|
|
||||||
dev_act_q1 NUMERIC, dev_act_q2 NUMERIC, dev_act_q3 NUMERIC, dev_act_q4 NUMERIC, dev_act_year NUMERIC,
|
|
||||||
total_act_year NUMERIC,
|
|
||||||
supp_corr_q2 NUMERIC, supp_corr_q3 NUMERIC, supp_corr_q4 NUMERIC,
|
|
||||||
dev_corr_q2 NUMERIC, dev_corr_q3 NUMERIC, dev_corr_q4 NUMERIC
|
|
||||||
)
|
|
||||||
AS $$
|
|
||||||
#variable_conflict use_column
|
#variable_conflict use_column
|
||||||
DECLARE
|
DECLARE
|
||||||
v_ahr INT; v_cap INT; v_oper INT;
|
v_ahr INT; v_cap INT; v_oper INT;
|
||||||
@ -4227,8 +4213,8 @@ BEGIN
|
|||||||
FROM merged m
|
FROM merged m
|
||||||
ORDER BY m.sc;
|
ORDER BY m.sc;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql STABLE SET search_path = v3, pg_catalog;
|
$function$
|
||||||
|
;
|
||||||
-- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text);
|
-- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
||||||
@ -7787,3 +7773,406 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id INT)
|
||||||
|
RETURNS INTEGER
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_inserted INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT report_type INTO v_report_type
|
||||||
|
FROM v3.rf_project_report
|
||||||
|
WHERE id = p_report_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO v3.form3_phase
|
||||||
|
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
||||||
|
SELECT p_report_id, pt.phase_code, pt.role,
|
||||||
|
pt.column_keys, pt.opens_at, pt.closes_at
|
||||||
|
FROM v3.phase_template pt
|
||||||
|
WHERE pt.form_type = 'FORM_3'
|
||||||
|
AND pt.sheet = v_report_type
|
||||||
|
ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
||||||
|
RETURN v_inserted;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.user_in_report_org(p_user_id INT, p_report_id INT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_org INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT p.org_unit_id
|
||||||
|
INTO v_org
|
||||||
|
FROM v3.rf_project_report r
|
||||||
|
JOIN v3.project p ON p.id = r.project_id
|
||||||
|
WHERE r.id = p_report_id;
|
||||||
|
|
||||||
|
IF v_org IS NULL THEN
|
||||||
|
RETURN TRUE; -- проект без org_unit — пропускаем фильтр
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM v3.user_org
|
||||||
|
WHERE user_id = p_user_id
|
||||||
|
AND org_unit_id = v_org
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.editable_columns_for3(
|
||||||
|
p_report_id INT,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
column_key TEXT,
|
||||||
|
closes_at TIMESTAMPTZ
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_role VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
|
||||||
|
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_role := v3.user_role_code(p_user_id);
|
||||||
|
IF v_role IS NULL THEN
|
||||||
|
RETURN; -- неизвестный/неактивный юзер → пустая маска
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ADMIN: UNION всех column_keys всех фаз отчёта.
|
||||||
|
IF v_role = 'ADMIN' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT ck AS column_key,
|
||||||
|
MAX(fp.closes_at) AS closes_at
|
||||||
|
FROM v3.form3_phase fp,
|
||||||
|
LATERAL unnest(fp.column_keys) AS ck
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
GROUP BY ck;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- EXECUTOR_RF без назначения → пустая маска (default deny).
|
||||||
|
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT ck AS column_key,
|
||||||
|
MAX(fp.closes_at) AS closes_at
|
||||||
|
FROM v3.form3_phase fp,
|
||||||
|
LATERAL unnest(fp.column_keys) AS ck
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND now() BETWEEN fp.opens_at AND fp.closes_at
|
||||||
|
GROUP BY ck;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.can_edit3(
|
||||||
|
p_report_id INT,
|
||||||
|
p_column_key TEXT,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS JSONB
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_role VARCHAR;
|
||||||
|
v_active_close TIMESTAMPTZ;
|
||||||
|
v_role_has_col BOOLEAN;
|
||||||
|
v_next_open TIMESTAMPTZ;
|
||||||
|
v_last_close TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
|
||||||
|
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_role := v3.user_role_code(p_user_id);
|
||||||
|
IF v_role IS NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'role_not_allowed',
|
||||||
|
'detail', 'unknown or inactive user'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ADMIN — bypass всех проверок.
|
||||||
|
IF v_role = 'ADMIN' THEN
|
||||||
|
RETURN jsonb_build_object('ok', true, 'admin', true);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- EXECUTOR_RF: фильтр по user_org проекта отчёта.
|
||||||
|
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'org_not_assigned',
|
||||||
|
'detail', p_column_key
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли?
|
||||||
|
SELECT MAX(fp.closes_at)
|
||||||
|
INTO v_active_close
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND now() BETWEEN fp.opens_at AND fp.closes_at;
|
||||||
|
|
||||||
|
IF v_active_close IS NOT NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', true,
|
||||||
|
'closes_at', v_active_close
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли на этом отчёте?
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
) INTO v_role_has_col;
|
||||||
|
|
||||||
|
IF NOT v_role_has_col THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'role_not_allowed',
|
||||||
|
'detail', p_column_key
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 3. Колонка доступна, но окно не активно. Подсказать ближайшее.
|
||||||
|
SELECT MIN(fp.opens_at)
|
||||||
|
INTO v_next_open
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND fp.opens_at > now();
|
||||||
|
|
||||||
|
IF v_next_open IS NOT NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'window_closed',
|
||||||
|
'detail', p_column_key,
|
||||||
|
'opens_at', v_next_open
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT MAX(fp.closes_at)
|
||||||
|
INTO v_last_close
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND fp.closes_at <= now();
|
||||||
|
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'window_closed',
|
||||||
|
'detail', p_column_key,
|
||||||
|
'closes_at', v_last_close
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
-- ─── 3. Authz-обёртки upd_form3_cell/cells (доп. сигнатуры с p_user_id) ─────
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form3_cells(
|
||||||
|
p_report_id INT,
|
||||||
|
p_changes JSONB,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_change JSONB;
|
||||||
|
v_column TEXT;
|
||||||
|
v_decision JSONB;
|
||||||
|
v_msg TEXT;
|
||||||
|
BEGIN
|
||||||
|
IF p_user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
|
||||||
|
RAISE EXCEPTION 'p_changes must be a JSONB array';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
||||||
|
LOOP
|
||||||
|
v_column := v_change->>'column';
|
||||||
|
IF v_column IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'change must have column: %', v_change;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_decision := v3.can_edit3(p_report_id, v_column, p_user_id);
|
||||||
|
|
||||||
|
IF NOT (v_decision->>'ok')::boolean THEN
|
||||||
|
IF v_decision->>'code' = 'role_not_allowed' THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: %', v_column;
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'org_not_assigned' THEN
|
||||||
|
RAISE EXCEPTION 'org_not_assigned: %', v_column;
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'window_closed' THEN
|
||||||
|
IF v_decision ? 'opens_at' THEN
|
||||||
|
v_msg := format('window_closed: %s (opens %s)',
|
||||||
|
v_column, v_decision->>'opens_at');
|
||||||
|
ELSIF v_decision->>'closes_at' IS NOT NULL THEN
|
||||||
|
v_msg := format('window_closed: %s (closed %s)',
|
||||||
|
v_column, v_decision->>'closes_at');
|
||||||
|
ELSE
|
||||||
|
v_msg := format('window_closed: %s (no phase)', v_column);
|
||||||
|
END IF;
|
||||||
|
RAISE EXCEPTION '%', v_msg;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form3_cell(
|
||||||
|
p_report_id INT,
|
||||||
|
p_line_id INT,
|
||||||
|
p_column TEXT,
|
||||||
|
p_value JSONB,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
SELECT * FROM v3.upd_form3_cells(
|
||||||
|
p_report_id,
|
||||||
|
jsonb_build_array(jsonb_build_object(
|
||||||
|
'line_id', p_line_id,
|
||||||
|
'column', p_column,
|
||||||
|
'value', p_value
|
||||||
|
)),
|
||||||
|
p_user_id
|
||||||
|
);
|
||||||
|
$function$
|
||||||
|
LANGUAGE sql VOLATILE;
|
||||||
|
|
||||||
|
-- ─── 4. add_project — раскладка snapshot обоих отчётов ──────────────────────
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_year INT,
|
||||||
|
p_org_unit_id INT,
|
||||||
|
p_level VARCHAR DEFAULT 'project',
|
||||||
|
p_parent_id INT DEFAULT NULL,
|
||||||
|
p_project_type VARCHAR DEFAULT NULL,
|
||||||
|
p_vsp_format VARCHAR DEFAULT NULL,
|
||||||
|
p_placement_type VARCHAR DEFAULT NULL,
|
||||||
|
p_object_address VARCHAR DEFAULT NULL,
|
||||||
|
p_staff_count INT DEFAULT NULL,
|
||||||
|
p_total_area NUMERIC DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
limit_report_id INT,
|
||||||
|
current_expenses_report_id INT
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_lim INT;
|
||||||
|
v_cur INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_year IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_year обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_org_unit_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
|
||||||
|
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||||
|
END IF;
|
||||||
|
IF p_parent_id IS NOT NULL AND
|
||||||
|
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO v3.project (
|
||||||
|
name, level, parent_id, org_unit_id,
|
||||||
|
project_type, vsp_format, placement_type, object_address, staff_count, total_area
|
||||||
|
) VALUES (
|
||||||
|
p_name, p_level, p_parent_id, p_org_unit_id,
|
||||||
|
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||||||
|
VALUES (v_pid, p_year, 'LIMIT') RETURNING id INTO v_lim;
|
||||||
|
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||||||
|
VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;
|
||||||
|
|
||||||
|
-- Snapshot RBAC-фаз (иначе default deny — отчёт read-only).
|
||||||
|
PERFORM v3.copy_template_to_report(v_lim);
|
||||||
|
PERFORM v3.copy_template_to_report(v_cur);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT_CREATE',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'year', p_year,
|
||||||
|
'level', p_level,
|
||||||
|
'parent_id', p_parent_id,
|
||||||
|
'org_unit_id', p_org_unit_id,
|
||||||
|
'project_type', p_project_type,
|
||||||
|
'vsp_format', p_vsp_format,
|
||||||
|
'placement_type', p_placement_type,
|
||||||
|
'object_address', p_object_address,
|
||||||
|
'staff_count', p_staff_count,
|
||||||
|
'total_area', p_total_area,
|
||||||
|
'limit_report_id', v_lim,
|
||||||
|
'current_expenses_report_id', v_cur
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
|||||||
@ -4251,3 +4251,28 @@ INSERT INTO v3.expense_item_form_type (expense_item_id,form_type_code) VALUES
|
|||||||
(798,'FORM_3');
|
(798,'FORM_3');
|
||||||
|
|
||||||
REFRESH MATERIALIZED VIEW CONCURRENTLY v3.mv_expense_item_tree;
|
REFRESH MATERIALIZED VIEW CONCURRENTLY v3.mv_expense_item_tree;
|
||||||
|
|
||||||
|
|
||||||
|
-- ─── 5. Seed фаз FORM_3 (идемпотентно, только FORM_3) ──────────────────────
|
||||||
|
-- DELETE скоупится на FORM_3 → фазы FORM_1/2/4 не затрагиваются.
|
||||||
|
DELETE FROM v3.phase_template WHERE form_type = 'FORM_3';
|
||||||
|
|
||||||
|
INSERT INTO v3.phase_template
|
||||||
|
(form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||||||
|
SELECT 'FORM_3', sh, 'RF_FILL_2026', 'EXECUTOR_RF',
|
||||||
|
ARRAY[
|
||||||
|
'q1.adj_by_items','q1.adj_increase','q1.m1','q1.m2','q1.m3',
|
||||||
|
'q2.adj_by_items','q2.adj_increase','q2.m1','q2.m2','q2.m3',
|
||||||
|
'q3.adj_by_items','q3.adj_increase','q3.m1','q3.m2','q3.m3',
|
||||||
|
'q4.adj_by_items','q4.adj_increase','q4.m1','q4.m2','q4.m3','q4.spod'
|
||||||
|
],
|
||||||
|
'2026-01-01 00:00:00+00'::timestamptz,
|
||||||
|
'2026-12-31 23:59:59+00'::timestamptz
|
||||||
|
FROM (VALUES ('LIMIT'), ('CURRENT_EXPENSES')) AS s(sh);
|
||||||
|
|
||||||
|
-- ─── 6. Backfill v3.form3_phase для существующих отчётов ────────────────────
|
||||||
|
-- ON CONFLICT DO NOTHING внутри copy_template_to_report → повторный прогон
|
||||||
|
-- ничего не дублирует.
|
||||||
|
SELECT id, v3.copy_template_to_report(id) AS phases_inserted
|
||||||
|
FROM v3.rf_project_report
|
||||||
|
ORDER BY id;
|
||||||
@ -6,7 +6,7 @@ from src.core.errors import AccessDeniedException
|
|||||||
from src.api.v1.deps import get_current_active_user, require_admin
|
from src.api.v1.deps import get_current_active_user, require_admin
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitBaseSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
|
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitCreateSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
|
||||||
from src.services.org_unit_service import OrgUnitService
|
from src.services.org_unit_service import OrgUnitService
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ async def org_unit_id(
|
|||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def create_ssp(
|
async def create_ssp(
|
||||||
org_data: OrgUnitBaseSchema,
|
org_data: OrgUnitCreateSchema,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(require_admin),
|
current_user: AppUser = Depends(require_admin),
|
||||||
) -> BaseSingleResponse[OrgUnitSchema]:
|
) -> BaseSingleResponse[OrgUnitSchema]:
|
||||||
|
|||||||
@ -14,6 +14,8 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.services.rf_project_report_service import RfProjectReportService
|
||||||
|
from src.services.rf_project_report_line_service import RfProjectReportLineService
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
from src.services.budget_line_service import BudgetLineService
|
from src.services.budget_line_service import BudgetLineService
|
||||||
from src.api.v1.deps import get_user_by_token
|
from src.api.v1.deps import get_user_by_token
|
||||||
@ -490,7 +492,8 @@ class FormEventProcess:
|
|||||||
class ProjectEventProcess:
|
class ProjectEventProcess:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.project_service: ProjectService = ProjectService(db)
|
self.project_service: ProjectService = ProjectService(db)
|
||||||
self.bf_service: BudgetFormService = BudgetFormService(db)
|
self.prl_service: RfProjectReportLineService = RfProjectReportLineService(db)
|
||||||
|
self.pr_service: RfProjectReportService = RfProjectReportService(db)
|
||||||
self.user_service: UserService = UserService(db)
|
self.user_service: UserService = UserService(db)
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
@ -573,11 +576,16 @@ class ProjectEventProcess:
|
|||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project_report = await self.pr_service.get(
|
||||||
if not project:
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
if not project_report:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await self.project_service.add_form3_line(
|
ids = await self.prl_service.get_ids(project_report_id=project_report.id)
|
||||||
|
result = await self.project_service.add_form3_line(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
@ -585,6 +593,16 @@ class ProjectEventProcess:
|
|||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
final_result = {
|
||||||
|
"data": result,
|
||||||
|
"new_line_id": None,
|
||||||
|
}
|
||||||
|
for el in result:
|
||||||
|
if el[3]["line_id"] and el[3]["line_id"] not in ids:
|
||||||
|
final_result["new_line_id"] = el[3]["line_id"]
|
||||||
|
return final_result
|
||||||
|
return final_result
|
||||||
|
|
||||||
async def __del_row(
|
async def __del_row(
|
||||||
self,
|
self,
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
@ -595,7 +613,7 @@ class ProjectEventProcess:
|
|||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"line_id": int,
|
"row_id": int,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project = await self.project_service.get(project_id=project_id, user=user)
|
||||||
@ -606,7 +624,7 @@ class ProjectEventProcess:
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
line_id=event_data["line_id"],
|
line_id=event_data["row_id"],
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -776,6 +794,7 @@ async def process_websocket(
|
|||||||
async with get_db_session(user_id=user_id) as db:
|
async with get_db_session(user_id=user_id) as db:
|
||||||
processor = processor_cls(db)
|
processor = processor_cls(db)
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
data_old = data.copy()
|
||||||
data["result"] = await processor.process(
|
data["result"] = await processor.process(
|
||||||
event_data=data,
|
event_data=data,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@ -789,7 +808,7 @@ async def process_websocket(
|
|||||||
"time_ms": f"{db_ms:.2f}",
|
"time_ms": f"{db_ms:.2f}",
|
||||||
"event": data.get("event"),
|
"event": data.get("event"),
|
||||||
}
|
}
|
||||||
extra.update(data)
|
extra.update(data_old)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"db_time: {extra}",
|
f"db_time: {extra}",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from src.db.models.collegial_approval import CollegialApproval
|
|||||||
from src.db.models.contract_detail import ContractDetail
|
from src.db.models.contract_detail import ContractDetail
|
||||||
from src.db.models.contract_summary import ContractSummary
|
from src.db.models.contract_summary import ContractSummary
|
||||||
from src.db.models.expense_item import ExpenseItem
|
from src.db.models.expense_item import ExpenseItem
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
from src.db.models.form_limit import FormLimit
|
from src.db.models.form_limit import FormLimit
|
||||||
from src.db.models.form_phase import FormPhase
|
from src.db.models.form_phase import FormPhase
|
||||||
from src.db.models.form_type import FormType
|
from src.db.models.form_type import FormType
|
||||||
|
|||||||
27
api/src/db/models/form3_phase.py
Normal file
27
api/src/db/models/form3_phase.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import ARRAY, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Form3Phase(Base):
|
||||||
|
__tablename__ = "form3_phase"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(text("cardinality(column_keys) > 0"), name="chk_v3_form3_phase_columns"),
|
||||||
|
CheckConstraint(text("role != 'ADMIN'"), name="chk_v3_form3_phase_no_admin"),
|
||||||
|
CheckConstraint(text("closes_at > opens_at"), name="chk_v3_form3_phase_window"),
|
||||||
|
Index("ix_v3_form3_phase_active", "rf_project_report_id", "role", "opens_at", "closes_at"),
|
||||||
|
Index("ix_v3_form3_phase_columns_gin", "column_keys", postgresql_using="gin"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rf_project_report_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.rf_project_report.id", ondelete="CASCADE"), primary_key=True
|
||||||
|
)
|
||||||
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
@ -233,6 +233,10 @@ class OrgUnitBaseSchema(BaseModel):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OrgUnitCreateSchema(OrgUnitBaseSchema):
|
||||||
|
title: str = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
is_ssp: Optional[bool] = None
|
is_ssp: Optional[bool] = None
|
||||||
@ -358,7 +362,7 @@ class AddProjectBody(BaseModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
)
|
)
|
||||||
year: int
|
year: int = Field(default_factory=lambda: datetime.now().year)
|
||||||
branch_id: int
|
branch_id: int
|
||||||
level: Literal["project", "program"] = "project"
|
level: Literal["project", "program"] = "project"
|
||||||
parent_id: Optional[int] = None
|
parent_id: Optional[int] = None
|
||||||
@ -370,6 +374,10 @@ class AddProjectBody(BaseModel):
|
|||||||
staff_count: Optional[int] = None
|
staff_count: Optional[int] = None
|
||||||
total_area: Optional[float] = None
|
total_area: Optional[float] = None
|
||||||
|
|
||||||
|
@field_validator("year", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def optional_year(cls, v: Any) -> Any:
|
||||||
|
return v or datetime.now().year
|
||||||
|
|
||||||
class AddLineSchema(BaseModel):
|
class AddLineSchema(BaseModel):
|
||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class BudgetLineRepository:
|
|||||||
query = select(BudgetLine).where(BudgetLine.id == budget_line_id).limit(1)
|
query = select(BudgetLine).where(BudgetLine.id == budget_line_id).limit(1)
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
async def get_list(self, budget_line_ids: list[int]) -> BudgetLine | None:
|
async def get_list(self, budget_line_ids: list[int]) -> list[BudgetLine]:
|
||||||
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
||||||
return (await self.db.execute(query)).scalars().all()
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class ProjectRepository:
|
|||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int) -> dict:
|
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int, years: list[InterruptedError] | None = None) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
@ -29,6 +29,7 @@ class ProjectRepository:
|
|||||||
"org_unit_id": project.org_unit_id,
|
"org_unit_id": project.org_unit_id,
|
||||||
"org_unit_name": org_unit_name,
|
"org_unit_name": org_unit_name,
|
||||||
"report_count": int(report_count),
|
"report_count": int(report_count),
|
||||||
|
"years": years,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -66,11 +67,19 @@ class ProjectRepository:
|
|||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.order_by(Project.id)
|
.order_by(Project.id)
|
||||||
@ -82,7 +91,7 @@ class ProjectRepository:
|
|||||||
query = query.limit(limit)
|
query = query.limit(limit)
|
||||||
|
|
||||||
rows = (await self.db.execute(query)).all()
|
rows = (await self.db.execute(query)).all()
|
||||||
payload = [self._serialize_project(row[0], row[1], row[2]) for row in rows]
|
payload = [self._serialize_project(row[0], row[1], row[2], row[3]) for row in rows]
|
||||||
if not with_count:
|
if not with_count:
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@ -101,11 +110,18 @@ class ProjectRepository:
|
|||||||
.where(RfProjectReport.project_id == Project.id)
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.where(Project.id == project_id)
|
.where(Project.id == project_id)
|
||||||
@ -117,7 +133,7 @@ class ProjectRepository:
|
|||||||
row = (await self.db.execute(query)).first()
|
row = (await self.db.execute(query)).first()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return self._serialize_project(row[0], row[1], row[2])
|
return self._serialize_project(row[0], row[1], row[2], row[3])
|
||||||
|
|
||||||
async def get_reports(self, project_id: int) -> list[dict]:
|
async def get_reports(self, project_id: int) -> list[dict]:
|
||||||
line_count_subq = (
|
line_count_subq = (
|
||||||
|
|||||||
39
api/src/repository/rf_project_report_line_repository.py
Normal file
39
api/src/repository/rf_project_report_line_repository.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(self, line_id: int) -> RfProjectReportLine | None:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id == line_id
|
||||||
|
).limit(1)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_list(self, line_ids: list[int]) -> list[RfProjectReportLine]:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id.in_(line_ids)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine.id
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.rf_project_report_id == project_report_id
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
33
api/src/repository/rf_project_report_repository.py
Normal file
33
api/src/repository/rf_project_report_repository.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> RfProjectReport | None:
|
||||||
|
assert report_id is not None or all((el is not None for el in (year, report_type, project_id)))
|
||||||
|
query = select(
|
||||||
|
RfProjectReport
|
||||||
|
)
|
||||||
|
if report_id is not None:
|
||||||
|
query = query.where(RfProjectReport.id == report_id)
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
RfProjectReport.year == year,
|
||||||
|
RfProjectReport.report_type == report_type,
|
||||||
|
RfProjectReport.project_id == project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.limit(1)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
34
api/src/services/rf_project_report_line_service.py
Normal file
34
api/src/services/rf_project_report_line_service.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
from src.repository.rf_project_report_line_repository import RfProjectReportLineRepository
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.prl_repo = RfProjectReportLineRepository(db)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_id: int,
|
||||||
|
) -> RfProjectReportLine | None:
|
||||||
|
return await self.prl_repo.get(line_id=line_id)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_ids: list[int],
|
||||||
|
) -> list[RfProjectReportLine]:
|
||||||
|
return await self.prl_repo.get_list(line_ids=line_ids)
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
return await self.prl_repo.get_ids(project_report_id=project_report_id)
|
||||||
31
api/src/services/rf_project_report_service.py
Normal file
31
api/src/services/rf_project_report_service.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
|
from src.repository.rf_project_report_repository import RfProjectReportRepository
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
from src.repository.rf_project_report_line_repository import RfProjectReportLineRepository
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.pr_repo = RfProjectReportRepository(db)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
user: AppUser| None = None,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> RfProjectReport | None:
|
||||||
|
return await self.pr_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
@ -11,6 +11,7 @@ from src.repository.budget_form_repository import BudgetFormRepository
|
|||||||
from src.core.errors import AccessDeniedException, ValidationException
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
from src.db.models import AppUser, UserRoleEnum, Vsp
|
from src.db.models import AppUser, UserRoleEnum, Vsp
|
||||||
from src.domain.schemas import VSPCreate, VSPUpdate
|
from src.domain.schemas import VSPCreate, VSPUpdate
|
||||||
|
from src.repository.org_unit_repository import OrgUnitRepository
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
from src.repository.vsp_repository import VSPRepository
|
from src.repository.vsp_repository import VSPRepository
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ class VSPService:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.vsp_repo = VSPRepository(db)
|
self.vsp_repo = VSPRepository(db)
|
||||||
self.form_repo = BudgetFormRepository(db)
|
self.form_repo = BudgetFormRepository(db)
|
||||||
|
self.org_unit_repo = OrgUnitRepository(db)
|
||||||
self.user_repo = UserRepository(db)
|
self.user_repo = UserRepository(db)
|
||||||
|
|
||||||
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
|
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
|
||||||
@ -128,6 +130,7 @@ class VSPService:
|
|||||||
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
|
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
|
||||||
if not self._can_modify(user):
|
if not self._can_modify(user):
|
||||||
raise AccessDeniedException()
|
raise AccessDeniedException()
|
||||||
|
await self._ensure_active_branch(payload.branch_id)
|
||||||
try:
|
try:
|
||||||
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
|
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
|
||||||
if created.closed_at is not None and created.closed_at <= date.today():
|
if created.closed_at is not None and created.closed_at <= date.today():
|
||||||
@ -245,6 +248,9 @@ class VSPService:
|
|||||||
if not data:
|
if not data:
|
||||||
return vsp
|
return vsp
|
||||||
|
|
||||||
|
if "branch_id" in data:
|
||||||
|
await self._ensure_active_branch(data["branch_id"])
|
||||||
|
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
ssp_ids = await self._get_scoped_ssp_ids(user=user)
|
ssp_ids = await self._get_scoped_ssp_ids(user=user)
|
||||||
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
|
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
|
||||||
@ -291,3 +297,10 @@ class VSPService:
|
|||||||
return
|
return
|
||||||
await self.vsp_repo.deactivate_by_close_date(as_of=today)
|
await self.vsp_repo.deactivate_by_close_date(as_of=today)
|
||||||
_last_info_close_sync_run = today
|
_last_info_close_sync_run = today
|
||||||
|
|
||||||
|
async def _ensure_active_branch(self, branch_id: int | None) -> None:
|
||||||
|
if branch_id is None:
|
||||||
|
return
|
||||||
|
org_unit = await self.org_unit_repo.get(org_unit_id=branch_id)
|
||||||
|
if org_unit is None or not org_unit.is_active:
|
||||||
|
raise ValidationException(description="Указан неактивный или отсутствующий ССП/РФ")
|
||||||
|
|||||||
@ -48,6 +48,31 @@ def _add_line(client, admin_tokens, auth_headers, project_id: int) -> int:
|
|||||||
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
|
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_smoke(client, admin_tokens, auth_headers):
|
||||||
|
name = f"PT_{uuid.uuid4().hex[:10]}"
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/projects",
|
||||||
|
json={"name": name, "year": None, "branch_id": 1},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "project_id" in payload
|
||||||
|
assert (project_id := payload["project_id"]) is not None
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/projects/{project_id}",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
assert "id" in payload["result"]
|
||||||
|
assert payload["result"]["id"] == project_id
|
||||||
|
assert "years" in payload["result"]
|
||||||
|
assert isinstance(payload["result"]["years"], list)
|
||||||
|
assert len(payload["result"]["years"]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_projects_write_smoke(client, admin_tokens, auth_headers):
|
def test_projects_write_smoke(client, admin_tokens, auth_headers):
|
||||||
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
|||||||
@ -150,6 +150,36 @@ def test_vsp_create_forbidden_for_executor(client, isp_tokens, auth_headers):
|
|||||||
assert "message" in payload
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_create_with_inactive_ssp_fails(client, admin_tokens, auth_headers):
|
||||||
|
created_org = client.post(
|
||||||
|
"/api/v1/org-unit/",
|
||||||
|
json={"title": f"Inactive SSP {uuid.uuid4().hex[:6]}", "is_ssp": True},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert created_org.status_code == 200
|
||||||
|
org_unit_id = created_org.json()["result"]["id"]
|
||||||
|
|
||||||
|
deactivate = client.patch(
|
||||||
|
f"/api/v1/org-unit/{org_unit_id}",
|
||||||
|
json={"is_active": False},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert deactivate.status_code == 200
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/dict/info",
|
||||||
|
json={
|
||||||
|
"registration_number": _unique_reg_number(),
|
||||||
|
"address": "Should Fail",
|
||||||
|
"ssp_id": org_unit_id,
|
||||||
|
},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
payload = response.json()
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
def test_vsp_update_smoke(client, admin_tokens, auth_headers):
|
def test_vsp_update_smoke(client, admin_tokens, auth_headers):
|
||||||
reg_number = _unique_reg_number()
|
reg_number = _unique_reg_number()
|
||||||
created = client.post(
|
created = client.post(
|
||||||
@ -195,7 +225,33 @@ def test_vsp_delete_smoke(client, admin_tokens, auth_headers):
|
|||||||
json={
|
json={
|
||||||
"registration_number": reg_number,
|
"registration_number": reg_number,
|
||||||
"address": "To Be Deleted",
|
"address": "To Be Deleted",
|
||||||
"ssp_id": 1,
|
"ssp_id": 2,
|
||||||
|
},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert created.status_code == 201
|
||||||
|
vsp_id = created.json()["result"]["id"]
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1/dict/info/{vsp_id}",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["message"] == "Запись INFO удалена"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_delete_with_close_date_smoke(client, admin_tokens, auth_headers):
|
||||||
|
reg_number = _unique_reg_number()
|
||||||
|
created = client.post(
|
||||||
|
"/api/v1/dict/info",
|
||||||
|
json={
|
||||||
|
"registration_number": reg_number,
|
||||||
|
"address": "To Be Deleted with close_date",
|
||||||
|
"ssp_id": 2,
|
||||||
|
"open_date": "2026-01-01",
|
||||||
|
"close_date": "2030-01-01",
|
||||||
},
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -26,7 +26,8 @@ SELECT setval('v3.budget_line_id_seq', 1);
|
|||||||
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
||||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
||||||
|
|
||||||
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,location_form,numbers,rent_contract_num,rent_end_date) VALUES
|
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
||||||
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','встроенное помещение',NULL,'',NULL),
|
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','',NULL),
|
||||||
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','аренда',155,'12-45','2026-10-02');
|
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','12-45','2026-10-02'),
|
||||||
SELECT setval('v3.vsp_id_seq', 2);
|
(3,2,'3434','победы 12','формат','2026-06-01','субаренда', NULL,34,NULL,true,'2026-07-01 13:32:35.327097',false,'2026-06-29 15:53:50.236062',91,'3434',NULL,'2','примечание','567','2026-07-01');
|
||||||
|
SELECT setval('v3.vsp_id_seq', 3);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const DictVspApi = {
|
|||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getDropdownVsp(params){
|
getDropdownVsp(params) {
|
||||||
return api.get(`/dict/info/dropdown`, params).then((r) => r.data);
|
return api.get(`/dict/info/dropdown`, { params }).then((r) => r.data);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
34
web/src/api/projects.js
Normal file
34
web/src/api/projects.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import api from "./client";
|
||||||
|
|
||||||
|
export const ProjectsApi = {
|
||||||
|
list: (params) => {
|
||||||
|
return api.get(`/projects`, { params }).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (id) => {
|
||||||
|
return api.get(`/projects/${id}`).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
create: (data) => {
|
||||||
|
return api.post("/projects", data).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
update: (id, data) => {
|
||||||
|
return api.patch(`/projects/${id}`, data).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: (id) => {
|
||||||
|
return api.delete(`/projects/${id}`).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTableData: (id, sheetName, year) => {
|
||||||
|
return api
|
||||||
|
.get(`/projects/${id}/report/${year}/${sheetName}`)
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
export: (id, year, sheetName) => {
|
||||||
|
return api.get(`/export/project/${id}/report/${year}/${sheetName}`, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -17,6 +17,7 @@ import { CircularProgress } from '@mui/material';
|
|||||||
import NewTablePage from '../pages/NewTablePage.jsx';
|
import NewTablePage from '../pages/NewTablePage.jsx';
|
||||||
import TablesTest from '../pages/TablesTest.jsx';
|
import TablesTest from '../pages/TablesTest.jsx';
|
||||||
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
||||||
|
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
||||||
|
|
||||||
export const PrivateRoute = () => {
|
export const PrivateRoute = () => {
|
||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
@ -37,8 +38,10 @@ export const AppRoutes = () => {
|
|||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
<Route path="/" element={<TasksPage />} />
|
<Route path="/" element={<TasksPage />} />
|
||||||
<Route path="/tasks" element={<TasksPage />} />
|
<Route path="/tasks" element={<TasksPage />} />
|
||||||
|
<Route path="/projects" element={<ProjectsPage />}/>
|
||||||
<Route path="/forms" element={<FormsPage />} />
|
<Route path="/forms" element={<FormsPage />} />
|
||||||
<Route path="/task/:taskId" element={<TaskPage />} />
|
<Route path="/task/:taskId" element={<TaskPage />} />
|
||||||
|
<Route path="/project/:projectId" element={<TaskPage />} />
|
||||||
<Route path="/table/:tableId" element={<TablePage />} />
|
<Route path="/table/:tableId" element={<TablePage />} />
|
||||||
<Route path="/table-temp/:tableId" element={<TableTempPage />} />
|
<Route path="/table-temp/:tableId" element={<TableTempPage />} />
|
||||||
<Route path="/admin_panel/users" element={<UsersPage />} />
|
<Route path="/admin_panel/users" element={<UsersPage />} />
|
||||||
@ -49,7 +52,7 @@ export const AppRoutes = () => {
|
|||||||
<Route path="/new-table" element={<NewTablePage />} />
|
<Route path="/new-table" element={<NewTablePage />} />
|
||||||
<Route path="/tables-new" element={<TablesTest />} />
|
<Route path="/tables-new" element={<TablesTest />} />
|
||||||
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
|
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
|
||||||
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction" element={<NewFormTablePage />} />
|
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction/:year" element={<NewFormTablePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { NavLink } from 'react-router-dom';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { TasksSvg, AdminPanelSvg, DictSvg } from '../common/icons/icons';
|
import { TasksSvg, AdminPanelSvg, DictSvg } from '../common/icons/icons';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../constants';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
|
|
||||||
const Switch = styled.div`
|
const Switch = styled.div`
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|||||||
@ -31,12 +31,13 @@ const LockBadge = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Функция для подсветки текста
|
// Функция для подсветки текста
|
||||||
const highlightText = (text, searchQuery) => {
|
const highlightText = (text, searchQueries) => {
|
||||||
if (!searchQuery || !text) return text;
|
if (!text) return text;
|
||||||
|
const cleanSearchQueries = searchQueries.filter(q => (q !== undefined && q !== ''));
|
||||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
if (cleanSearchQueries.length == 0) return text;
|
||||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
||||||
|
|
||||||
|
const escapedQueries = cleanSearchQueries.map(query => query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
|
const regex = new RegExp(`(${ escapedQueries.join('|')})`, 'gi');
|
||||||
const parts = text.split(regex);
|
const parts = text.split(regex);
|
||||||
|
|
||||||
return parts.map((part, index) =>
|
return parts.map((part, index) =>
|
||||||
@ -89,7 +90,7 @@ const formatNumber = (value, asInteger = false) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
|
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, color, isEditable }) => {
|
||||||
const { lockedCells } = useRealtime();
|
const { lockedCells } = useRealtime();
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
@ -105,9 +106,10 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
|
|||||||
}
|
}
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
return highlightText(textValue, globalFilter);
|
return highlightText(textValue, [globalFilter, columnFilter]);
|
||||||
}, [textValue, globalFilter]);
|
}, [textValue, globalFilter]);
|
||||||
|
|
||||||
|
|
||||||
// Базовые стили для заблокированной ячейки
|
// Базовые стили для заблокированной ячейки
|
||||||
const lockedStyles = isLocked ? {
|
const lockedStyles = isLocked ? {
|
||||||
border: '2px solid #e0e0e0',
|
border: '2px solid #e0e0e0',
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
import { useEffect, useRef, useLayoutEffect, useState } from 'react';
|
import { useEffect, useRef, useLayoutEffect, useState } from 'react';
|
||||||
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
||||||
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
||||||
|
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||||
|
|
||||||
|
const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
||||||
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
|
|
||||||
const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
|
||||||
const [curValue, setCurValue] = useState(value);
|
const [curValue, setCurValue] = useState(value);
|
||||||
const [displayValue, setDisplayValue] = useState(value);
|
const [displayValue, setDisplayValue] = useState(value);
|
||||||
const [isFormulaMode, setIsFormulaMode] = useState(false);
|
const [isFormulaMode, setIsFormulaMode] = useState(false);
|
||||||
@ -69,7 +72,6 @@ const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
|||||||
if (isFormula(curValue)) {
|
if (isFormula(curValue)) {
|
||||||
// Сохраняем формулу в localStorage
|
// Сохраняем формулу в localStorage
|
||||||
saveToStorage(storageKey, curValue);
|
saveToStorage(storageKey, curValue);
|
||||||
|
|
||||||
// Вычисляем и сохраняем результат
|
// Вычисляем и сохраняем результат
|
||||||
try {
|
try {
|
||||||
const formula = extractFormula(curValue);
|
const formula = extractFormula(curValue);
|
||||||
@ -108,6 +110,32 @@ const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
|||||||
return displayValue;
|
return displayValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const finishEditing = () => {
|
||||||
|
if (!disabled) {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
const editingCell = table.getState().editingCell || null;
|
||||||
|
if (editingCell) {
|
||||||
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
// Проверяем, был ли клик внутри элемента редактирования
|
||||||
|
const editElement = ref.current;
|
||||||
|
if (editElement && !editElement.contains(event.target)) {
|
||||||
|
finishEditing();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [ref, finishEditing]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@ -150,16 +178,10 @@ const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
|||||||
}}
|
}}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
value={curValue}
|
value={curValue}
|
||||||
onBlur={handleSave}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSave();
|
finishEditing();
|
||||||
// Закрываем редактирование
|
|
||||||
if (onChange) {
|
|
||||||
// Симулируем завершение редактирования
|
|
||||||
document.activeElement?.blur();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
||||||
|
|||||||
@ -26,11 +26,11 @@ import { useRealtime } from './contexts/RealtimeContext';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { additionVspRowTable } from './constants/addingRowConfig';
|
import { additionVspRowTable } from './constants/addingRowConfig';
|
||||||
import { SelectVspModal } from './Modals/selectVspModal';
|
import { SelectVspModal } from './Modals/SelectVspModal';
|
||||||
import { getRowId } from './utils/rowUtils';
|
import { getRowId } from './utils/rowUtils';
|
||||||
|
|
||||||
const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||||
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
|
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
const [selectedColumnId, setSelectedColumnId] = useState();
|
const [selectedColumnId, setSelectedColumnId] = useState();
|
||||||
@ -63,7 +63,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
addRow: contextAddRow,
|
addRow: contextAddRow,
|
||||||
deleteRow: contextDeleteRow,
|
deleteRow: contextDeleteRow,
|
||||||
startEditing: contextStartEditing,
|
startEditing: contextStartEditing,
|
||||||
endEditing: contextEndEditing,
|
|
||||||
} = useRealtime();
|
} = useRealtime();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -180,8 +179,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
if (cell) {
|
if (cell) {
|
||||||
if (editingCell) return;
|
if (editingCell) return;
|
||||||
contextStartEditing?.(cell.row, cell.column);
|
contextStartEditing?.(cell.row, cell.column);
|
||||||
} else {
|
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columnVirtualizerOptions: ({ table }) => ({
|
columnVirtualizerOptions: ({ table }) => ({
|
||||||
@ -229,6 +226,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
minHeight: '1px',
|
minHeight: '1px',
|
||||||
maxHeight: '1px',
|
maxHeight: '1px',
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
|
'& svg': {
|
||||||
|
height: '1px',
|
||||||
|
minHeight: '1px',
|
||||||
|
maxHeight: '1px',
|
||||||
|
visibility: 'hidden',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
muiTablePaperProps: getTablePaperStyles(),
|
muiTablePaperProps: getTablePaperStyles(),
|
||||||
@ -360,6 +363,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
|
year={year}
|
||||||
|
isProject={formType == 'PROJECT'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={tableWrapperStyle}>
|
<div style={tableWrapperStyle}>
|
||||||
@ -420,8 +425,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
onClose={() => setIsOpenModalSelectVsp(false)}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSelect={handleAddVspRow}
|
onSelect={handleAddVspRow}
|
||||||
// vspOptions={}
|
|
||||||
// selectedVSP={}
|
|
||||||
title="Выбор ВСП"
|
title="Выбор ВСП"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import CollapsibleTree from '../../common/CollapsibleTree';
|
|||||||
import ZoomSlider from './ZoomSlider';
|
import ZoomSlider from './ZoomSlider';
|
||||||
import SearchComponent from '../../common/SearchComponent';
|
import SearchComponent from '../../common/SearchComponent';
|
||||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||||
import { exportSheet } from '../../../utils/exportForm';
|
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
@ -39,6 +39,8 @@ const SettingPanel = ({
|
|||||||
formId,
|
formId,
|
||||||
sheetName,
|
sheetName,
|
||||||
direction,
|
direction,
|
||||||
|
year,
|
||||||
|
isProject,
|
||||||
}) => {
|
}) => {
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
@ -76,7 +78,16 @@ const SettingPanel = ({
|
|||||||
const handleClickDeleteFormula = () => { };
|
const handleClickDeleteFormula = () => { };
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (!formId || !sheetName || isExporting) return;
|
if (isExporting) return;
|
||||||
|
if (isProject) {
|
||||||
|
if (!formId || !sheetName || !year) return;
|
||||||
|
setIsExporting(true);
|
||||||
|
await exportSheetProject(formId, sheetName, year);
|
||||||
|
setIsExporting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formId || !sheetName) return;
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
await exportSheet(formId, sheetName, direction);
|
await exportSheet(formId, sheetName, direction);
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@ -115,40 +126,26 @@ const SettingPanel = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
|
|
||||||
<Divider orientation="vertical" flexItem />
|
{!isProject &&
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<GroupByObject title="Строки">
|
||||||
|
<Tooltip title="Добавить строку">
|
||||||
|
<IconButton onClick={onAddRow} variant="outlined">
|
||||||
|
<Plus />
|
||||||
|
<AddLine />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<GroupByObject title="Строки">
|
<Tooltip title="Удалить строку">
|
||||||
<Tooltip title="Добавить строку">
|
<IconButton onClick={onDeleteRow} variant="outlined">
|
||||||
<IconButton onClick={onAddRow} variant="outlined">
|
<Minus />
|
||||||
<Plus />
|
<RemoveLine />
|
||||||
<AddLine />
|
</IconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
</GroupByObject>
|
||||||
|
</>
|
||||||
<Tooltip title="Удалить строку">
|
}
|
||||||
<IconButton onClick={onDeleteRow} variant="outlined">
|
|
||||||
<Minus />
|
|
||||||
<RemoveLine />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</GroupByObject>
|
|
||||||
|
|
||||||
{/* <Divider orientation="vertical" flexItem />
|
|
||||||
|
|
||||||
<GroupByObject title="Формулы">
|
|
||||||
<Tooltip title="Добавить формулу">
|
|
||||||
<IconButton onClick={handleClickAddFormula} variant="outlined">
|
|
||||||
<FormulaSettingPanel />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Удалить формулу">
|
|
||||||
<IconButton onClick={handleClickDeleteFormula} variant="outlined">
|
|
||||||
<Minus />
|
|
||||||
<FormulaSettingPanel />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</GroupByObject> */}
|
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
<GroupByObject title="Столбцы">
|
<GroupByObject title="Столбцы">
|
||||||
{columns ? (
|
{columns ? (
|
||||||
|
|||||||
@ -313,10 +313,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
onChange={(e) => handleFilterChange(e.target.value)}
|
onChange={(e) => handleFilterChange(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '4px 6px',
|
padding: '0.25rem 0.375rem',
|
||||||
fontSize: '11px',
|
fontSize: '0.6875rem',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '4px',
|
borderRadius: '0.25rem',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -330,23 +330,30 @@ const FilterCell = ({ column, table }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'range': {
|
case 'range': {
|
||||||
// Для диапазона можно реализовать два поля
|
|
||||||
const [min, max] = column.getFilterValue() || ['', ''];
|
const [min, max] = column.getFilterValue() || ['', ''];
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div style={{ display: 'flex', gap: '0.25rem' }}> {/* 4px */}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="От"
|
placeholder="От"
|
||||||
value={min}
|
value={min}
|
||||||
onChange={(e) => column.setFilterValue([e.target.value, max])}
|
onChange={(e) => column.setFilterValue([e.target.value, max])}
|
||||||
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
style={{
|
||||||
|
width: '50%',
|
||||||
|
padding: '0.25rem',
|
||||||
|
fontSize: '0.6875rem'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="До"
|
placeholder="До"
|
||||||
value={max}
|
value={max}
|
||||||
onChange={(e) => column.setFilterValue([min, e.target.value])}
|
onChange={(e) => column.setFilterValue([min, e.target.value])}
|
||||||
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
style={{
|
||||||
|
width: '50%',
|
||||||
|
padding: '0.25rem',
|
||||||
|
fontSize: '0.6875rem'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -361,10 +368,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '4px 6px',
|
padding: '0.25rem 0.375rem',
|
||||||
fontSize: '11px',
|
fontSize: '0.6875rem',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '4px',
|
borderRadius: '0.25rem',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,555 +0,0 @@
|
|||||||
|
|
||||||
import {
|
|
||||||
greenColumn,
|
|
||||||
whiteColumn,
|
|
||||||
orangeColumn,
|
|
||||||
yellowColumn,
|
|
||||||
redColumn,
|
|
||||||
blueColumn,
|
|
||||||
} from '../columnColors';
|
|
||||||
export const config = {
|
|
||||||
"config":{
|
|
||||||
"colors":{
|
|
||||||
"kod_razdela_kod_razdela":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela"
|
|
||||||
},
|
|
||||||
"id_stati_id_stati":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_stati_id_stati"
|
|
||||||
},
|
|
||||||
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
|
||||||
},
|
|
||||||
"naimenovanie_naimenovanie":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie"
|
|
||||||
},
|
|
||||||
"korrektirovka_limita_po_statyam_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"korrektirovka_limita_po_statyam_smety"
|
|
||||||
},
|
|
||||||
"field_uvelichenie_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_uvelichenie_smety"
|
|
||||||
},
|
|
||||||
"field_za_yanvar":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_yanvar"
|
|
||||||
},
|
|
||||||
"field_za_fevral":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_fevral"
|
|
||||||
},
|
|
||||||
"field_itogo_i_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_i_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_aprel":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_aprel"
|
|
||||||
},
|
|
||||||
"field_za_may":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_may"
|
|
||||||
},
|
|
||||||
"field_itogo_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_iyul":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_iyul"
|
|
||||||
},
|
|
||||||
"field_za_avgust":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_avgust"
|
|
||||||
},
|
|
||||||
"field_itogo_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_oktyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_oktyabr"
|
|
||||||
},
|
|
||||||
"field_za_noyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_noyabr"
|
|
||||||
},
|
|
||||||
"field_spod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_spod"
|
|
||||||
},
|
|
||||||
"field_itogo_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal"
|
|
||||||
},
|
|
||||||
"itogo_itogo":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"itogo_itogo"
|
|
||||||
},
|
|
||||||
"fakt_zamart":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zamart"
|
|
||||||
},
|
|
||||||
"fakt_zaiyun":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zaiyun"
|
|
||||||
},
|
|
||||||
"fakt_zasentyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zasentyabr"
|
|
||||||
},
|
|
||||||
"fakt_zadekabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zadekabr"
|
|
||||||
},
|
|
||||||
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati_id_stati",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Корректировка лимита",
|
|
||||||
"accessorKey":"korrektirovka_limita",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"по статьям сметы *",
|
|
||||||
"accessorKey":"korrektirovka_limita_po_statyam_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"",
|
|
||||||
"accessorKey":"field",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"увеличение сметы **",
|
|
||||||
"accessorKey":"field_uvelichenie_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ЯНВАРЬ",
|
|
||||||
"accessorKey":"field_za_yanvar",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ФЕВРАЛЬ",
|
|
||||||
"accessorKey":"field_za_fevral",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого I квартал",
|
|
||||||
"accessorKey":"field_itogo_i_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АПРЕЛЬ",
|
|
||||||
"accessorKey":"field_za_aprel",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за МАЙ",
|
|
||||||
"accessorKey":"field_za_may",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого II квартал",
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ИЮЛЬ",
|
|
||||||
"accessorKey":"field_za_iyul",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АВГУСТ",
|
|
||||||
"accessorKey":"field_za_avgust",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого III квартал",
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ОКТЯБРЬ",
|
|
||||||
"accessorKey":"field_za_oktyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за НОЯБРЬ",
|
|
||||||
"accessorKey":"field_za_noyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"СПОД",
|
|
||||||
"accessorKey":"field_spod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого IV квартал",
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого",
|
|
||||||
"accessorKey":"itogo",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Итого",
|
|
||||||
"accessorKey":"itogo_itogo",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Факт",
|
|
||||||
"accessorKey":"fakt",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"заМАРТ",
|
|
||||||
"accessorKey":"fakt_zamart",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заИЮНЬ",
|
|
||||||
"accessorKey":"fakt_zaiyun",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заСЕНТЯБРЬ",
|
|
||||||
"accessorKey":"fakt_zasentyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заДЕКАБРЬ",
|
|
||||||
"accessorKey":"fakt_zadekabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,577 +0,0 @@
|
|||||||
|
|
||||||
import {
|
|
||||||
greenColumn,
|
|
||||||
whiteColumn,
|
|
||||||
orangeColumn,
|
|
||||||
yellowColumn,
|
|
||||||
redColumn,
|
|
||||||
blueColumn,
|
|
||||||
} from '../columnColors';
|
|
||||||
export const config = {
|
|
||||||
"config":{
|
|
||||||
"colors":{
|
|
||||||
"kod_razdela_kod_razdela":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela"
|
|
||||||
},
|
|
||||||
"id_stati_id_stati":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_stati_id_stati"
|
|
||||||
},
|
|
||||||
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
|
||||||
},
|
|
||||||
"naimenovanie_naimenovanie":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie"
|
|
||||||
},
|
|
||||||
"korrektirovka_plana_po_statyam_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"korrektirovka_plana_po_statyam_smety"
|
|
||||||
},
|
|
||||||
"field_uvelichenie_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_uvelichenie_smety"
|
|
||||||
},
|
|
||||||
"field_za_yanvar":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_yanvar"
|
|
||||||
},
|
|
||||||
"field_za_fevral":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_fevral"
|
|
||||||
},
|
|
||||||
"field_itogo_i_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_i_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_aprel":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_aprel"
|
|
||||||
},
|
|
||||||
"field_za_may":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_may"
|
|
||||||
},
|
|
||||||
"field_itogo_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_iyul":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_iyul"
|
|
||||||
},
|
|
||||||
"field_za_avgust":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_avgust"
|
|
||||||
},
|
|
||||||
"field_itogo_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_oktyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_oktyabr"
|
|
||||||
},
|
|
||||||
"field_za_noyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_noyabr"
|
|
||||||
},
|
|
||||||
"field_spod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_spod"
|
|
||||||
},
|
|
||||||
"field_itogo_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal"
|
|
||||||
},
|
|
||||||
"itogo_korrektirovka_itogo_korrektirovka":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"itogo_korrektirovka_itogo_korrektirovka"
|
|
||||||
},
|
|
||||||
"fakt_zamart":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zamart"
|
|
||||||
},
|
|
||||||
"fakt_zaiyun":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zaiyun"
|
|
||||||
},
|
|
||||||
"fakt_zasentyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zasentyabr"
|
|
||||||
},
|
|
||||||
"fakt_zadekabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zadekabr"
|
|
||||||
},
|
|
||||||
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
|
||||||
},
|
|
||||||
"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal"
|
|
||||||
},
|
|
||||||
"itogo_skorrek_smeta_itogo_skorrek_smeta":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"itogo_skorrek_smeta_itogo_skorrek_smeta"
|
|
||||||
},
|
|
||||||
"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal"
|
|
||||||
},
|
|
||||||
"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati_id_stati",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Корректировка плана",
|
|
||||||
"accessorKey":"korrektirovka_plana",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"по статьям сметы *",
|
|
||||||
"accessorKey":"korrektirovka_plana_po_statyam_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"",
|
|
||||||
"accessorKey":"field",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"увеличение сметы **",
|
|
||||||
"accessorKey":"field_uvelichenie_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ЯНВАРЬ",
|
|
||||||
"accessorKey":"field_za_yanvar",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ФЕВРАЛЬ",
|
|
||||||
"accessorKey":"field_za_fevral",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого I квартал",
|
|
||||||
"accessorKey":"field_itogo_i_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АПРЕЛЬ",
|
|
||||||
"accessorKey":"field_za_aprel",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за МАЙ",
|
|
||||||
"accessorKey":"field_za_may",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого II квартал",
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ИЮЛЬ",
|
|
||||||
"accessorKey":"field_za_iyul",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АВГУСТ",
|
|
||||||
"accessorKey":"field_za_avgust",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого III квартал",
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ОКТЯБРЬ",
|
|
||||||
"accessorKey":"field_za_oktyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за НОЯБРЬ",
|
|
||||||
"accessorKey":"field_za_noyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"СПОД",
|
|
||||||
"accessorKey":"field_spod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого IV квартал",
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого корректировка",
|
|
||||||
"accessorKey":"itogo_korrektirovka",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Итого корректировка",
|
|
||||||
"accessorKey":"itogo_korrektirovka_itogo_korrektirovka",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Факт",
|
|
||||||
"accessorKey":"fakt",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"заМАРТ",
|
|
||||||
"accessorKey":"fakt_zamart",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заИЮНЬ",
|
|
||||||
"accessorKey":"fakt_zaiyun",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заСЕНТЯБРЬ",
|
|
||||||
"accessorKey":"fakt_zasentyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заДЕКАБРЬ",
|
|
||||||
"accessorKey":"fakt_zadekabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на II квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на II квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого скоррек. смета",
|
|
||||||
"accessorKey":"itogo_skorrek_smeta",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Итого скоррек. смета",
|
|
||||||
"accessorKey":"itogo_skorrek_smeta_itogo_skorrek_smeta",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на III квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на III квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на IV квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Смета расходов по развитию на IV квартал",
|
|
||||||
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,555 +0,0 @@
|
|||||||
|
|
||||||
import {
|
|
||||||
greenColumn,
|
|
||||||
whiteColumn,
|
|
||||||
orangeColumn,
|
|
||||||
yellowColumn,
|
|
||||||
redColumn,
|
|
||||||
blueColumn,
|
|
||||||
} from '../columnColors';
|
|
||||||
export const config = {
|
|
||||||
"config":{
|
|
||||||
"colors":{
|
|
||||||
"kod_razdela_kod_razdela":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela"
|
|
||||||
},
|
|
||||||
"id_stati_id_stati":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_stati_id_stati"
|
|
||||||
},
|
|
||||||
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
|
||||||
},
|
|
||||||
"naimenovanie_naimenovanie":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie"
|
|
||||||
},
|
|
||||||
"korrektirovka_limita_po_statyam_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"korrektirovka_limita_po_statyam_smety"
|
|
||||||
},
|
|
||||||
"field_uvelichenie_smety":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_uvelichenie_smety"
|
|
||||||
},
|
|
||||||
"field_za_yanvar":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_yanvar"
|
|
||||||
},
|
|
||||||
"field_za_fevral":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_fevral"
|
|
||||||
},
|
|
||||||
"field_itogo_i_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_i_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_aprel":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_aprel"
|
|
||||||
},
|
|
||||||
"field_za_may":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_may"
|
|
||||||
},
|
|
||||||
"field_itogo_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_iyul":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_iyul"
|
|
||||||
},
|
|
||||||
"field_za_avgust":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_avgust"
|
|
||||||
},
|
|
||||||
"field_itogo_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal"
|
|
||||||
},
|
|
||||||
"field_za_oktyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_oktyabr"
|
|
||||||
},
|
|
||||||
"field_za_noyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_za_noyabr"
|
|
||||||
},
|
|
||||||
"field_spod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_spod"
|
|
||||||
},
|
|
||||||
"field_itogo_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal"
|
|
||||||
},
|
|
||||||
"itogo_itogo":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"itogo_itogo"
|
|
||||||
},
|
|
||||||
"fakt_zamart":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zamart"
|
|
||||||
},
|
|
||||||
"fakt_zaiyun":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zaiyun"
|
|
||||||
},
|
|
||||||
"fakt_zasentyabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zasentyabr"
|
|
||||||
},
|
|
||||||
"fakt_zadekabr":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"fakt_zadekabr"
|
|
||||||
},
|
|
||||||
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal"
|
|
||||||
},
|
|
||||||
"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal":{
|
|
||||||
"color_type":whiteColumn,
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Код раздела",
|
|
||||||
"accessorKey":"kod_razdela_kod_razdela",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID статьи",
|
|
||||||
"accessorKey":"id_stati_id_stati",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"ID группы номенклатуры",
|
|
||||||
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Наименование",
|
|
||||||
"accessorKey":"naimenovanie_naimenovanie",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Корректировка лимита",
|
|
||||||
"accessorKey":"korrektirovka_limita",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"по статьям сметы *",
|
|
||||||
"accessorKey":"korrektirovka_limita_po_statyam_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"",
|
|
||||||
"accessorKey":"field",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"увеличение сметы **",
|
|
||||||
"accessorKey":"field_uvelichenie_smety",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ЯНВАРЬ",
|
|
||||||
"accessorKey":"field_za_yanvar",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ФЕВРАЛЬ",
|
|
||||||
"accessorKey":"field_za_fevral",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого I квартал",
|
|
||||||
"accessorKey":"field_itogo_i_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АПРЕЛЬ",
|
|
||||||
"accessorKey":"field_za_aprel",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за МАЙ",
|
|
||||||
"accessorKey":"field_za_may",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого II квартал",
|
|
||||||
"accessorKey":"field_itogo_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ИЮЛЬ",
|
|
||||||
"accessorKey":"field_za_iyul",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за АВГУСТ",
|
|
||||||
"accessorKey":"field_za_avgust",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого III квартал",
|
|
||||||
"accessorKey":"field_itogo_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за ОКТЯБРЬ",
|
|
||||||
"accessorKey":"field_za_oktyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"за НОЯБРЬ",
|
|
||||||
"accessorKey":"field_za_noyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"СПОД",
|
|
||||||
"accessorKey":"field_spod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого IV квартал",
|
|
||||||
"accessorKey":"field_itogo_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Итого",
|
|
||||||
"accessorKey":"itogo",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Итого",
|
|
||||||
"accessorKey":"itogo_itogo",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Факт",
|
|
||||||
"accessorKey":"fakt",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"заМАРТ",
|
|
||||||
"accessorKey":"fakt_zamart",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заИЮНЬ",
|
|
||||||
"accessorKey":"fakt_zaiyun",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заСЕНТЯБРЬ",
|
|
||||||
"accessorKey":"fakt_zasentyabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"заДЕКАБРЬ",
|
|
||||||
"accessorKey":"fakt_zadekabr",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains",
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"экономия (+) перерасход (-)",
|
|
||||||
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
|
||||||
"columns":[
|
|
||||||
{
|
|
||||||
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
|
||||||
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
|
||||||
"size":150,
|
|
||||||
"filterFn":"contains"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"muiTableHeadCellProps":{
|
|
||||||
"sx":{
|
|
||||||
"backgroundColor":"#C2D69B",
|
|
||||||
"color":"#000000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
1003
web/src/components/RealtimeTable/constants/PROJECT/LIMIT.js
Normal file
1003
web/src/components/RealtimeTable/constants/PROJECT/LIMIT.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -25,17 +25,29 @@ export const RealtimeProvider = ({
|
|||||||
formId,
|
formId,
|
||||||
sheetName,
|
sheetName,
|
||||||
direction,
|
direction,
|
||||||
|
year,
|
||||||
userId,
|
userId,
|
||||||
|
isProject = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||||
const wsUrl = `${(process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || "ws://localhost:8000").replace(/^http/, "ws")}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`;
|
const wsUrl =
|
||||||
|
`${(
|
||||||
|
process.env.REACT_APP_API_URL ||
|
||||||
|
process.env.REACT_APP_API_URL ||
|
||||||
|
process.env.REACT_APP_ROOT_PATH ||
|
||||||
|
"ws://localhost:8000"
|
||||||
|
).replace(/^http/, "ws")}` +
|
||||||
|
(!isProject
|
||||||
|
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`
|
||||||
|
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`);
|
||||||
|
|
||||||
//для ячеек которые редактируются другими пользователями
|
//для ячеек которые редактируются другими пользователями
|
||||||
const [lockedCells, setLockedCells] = useState([]);
|
const [lockedCells, setLockedCells] = useState([]);
|
||||||
const handleMessage = useCallback((data) => {
|
const handleMessage = useCallback((data) => {
|
||||||
// Обработка ошибок
|
// Обработка ошибок
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.log(data.error.message)
|
console.log(data.error.message);
|
||||||
toast.error("Server error:" + data.error.message || '');
|
toast.error("Server error:" + data.error.message || "");
|
||||||
onErrorRef.current?.(data.error);
|
onErrorRef.current?.(data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -81,7 +93,7 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
||||||
wsUrl,
|
wsUrl,
|
||||||
handleMessage,
|
handleMessage,
|
||||||
);
|
);
|
||||||
@ -145,13 +157,26 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, [isConnected]);
|
}, [isConnected]);
|
||||||
|
|
||||||
|
const addCommonField = (message) => {
|
||||||
|
if (isProject) {
|
||||||
|
message.report_type = sheetName;
|
||||||
|
message.project_id = formId;
|
||||||
|
message.year = year;
|
||||||
|
} else {
|
||||||
|
message.sheet = sheetName;
|
||||||
|
message.form_id = formId;
|
||||||
|
message.direction = direction;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
// Функция для начала редактирования ячейки
|
// Функция для начала редактирования ячейки
|
||||||
const startEditing = useCallback(
|
const startEditing = useCallback(
|
||||||
async (row, column) => {
|
async (row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -183,7 +208,7 @@ export const RealtimeProvider = ({
|
|||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -213,7 +238,7 @@ export const RealtimeProvider = ({
|
|||||||
async (row, column, value) => {
|
async (row, column, value) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -241,7 +266,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -249,10 +274,9 @@ export const RealtimeProvider = ({
|
|||||||
const message = {
|
const message = {
|
||||||
event: "row_added",
|
event: "row_added",
|
||||||
data: rowData,
|
data: rowData,
|
||||||
sheet: sheetName,
|
|
||||||
form_id: formId,
|
|
||||||
direction: direction,
|
|
||||||
};
|
};
|
||||||
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
@ -260,7 +284,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const deleteRow = useCallback(
|
const deleteRow = useCallback(
|
||||||
async (rowId) => {
|
async (rowId) => {
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -268,10 +292,8 @@ export const RealtimeProvider = ({
|
|||||||
const message = {
|
const message = {
|
||||||
event: "row_deleted",
|
event: "row_deleted",
|
||||||
data: { row_id: rowId },
|
data: { row_id: rowId },
|
||||||
sheet: sheetName,
|
|
||||||
form_id: formId,
|
|
||||||
direction: direction,
|
|
||||||
};
|
};
|
||||||
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { useRealtime } from "../contexts/RealtimeContext";
|
|||||||
import { deleteRow, insertCell, updateCells } from "../utils/cellUtils";
|
import { deleteRow, insertCell, updateCells } from "../utils/cellUtils";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { isVspNewRow, isVspRow } from "../utils/rowUtils";
|
import { isVspNewRow, isVspRow } from "../utils/rowUtils";
|
||||||
|
import { ProjectsApi } from "../../../api/projects";
|
||||||
|
|
||||||
const useRealtimeData = (formId, sheetName, direction) => {
|
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@ -77,7 +78,10 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
let res;
|
let res;
|
||||||
if (direction !== null) {
|
|
||||||
|
if(formType == 'PROJECT'){
|
||||||
|
res = await ProjectsApi.getTableData(formId, sheetName, year)
|
||||||
|
} else if (direction !== null) {
|
||||||
res = await FormsSheetApi.get(formId, sheetName, {
|
res = await FormsSheetApi.get(formId, sheetName, {
|
||||||
params: { direction: direction },
|
params: { direction: direction },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -107,30 +107,16 @@ export const useWebSocket = (url, onMessage) => {
|
|||||||
return false;
|
return false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = useCallback(
|
|
||||||
(token) => {
|
|
||||||
if (!token) {
|
|
||||||
console.warn("No token provided for login");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return sendMessage({
|
|
||||||
event: "user_login",
|
|
||||||
data: { token },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[sendMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect();
|
connect();
|
||||||
return () => disconnect();
|
return () => disconnect();
|
||||||
}, [connect, disconnect]);
|
}, [connect, disconnect]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isConnected: isConnectedRef,
|
isConnectedRef,
|
||||||
|
isConnected,
|
||||||
error,
|
error,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
login,
|
|
||||||
disconnect,
|
disconnect,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
|||||||
import { columns as mockColumns } from '../../mocks/mockTable';
|
import { columns as mockColumns } from '../../mocks/mockTable';
|
||||||
import { config } from './constants/FORM_2/AHR';
|
import { config } from './constants/FORM_2/AHR';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
import { useRealtime } from './contexts/RealtimeContext';
|
||||||
|
|
||||||
function EditCellPortal({
|
function EditCellPortal({
|
||||||
cell,
|
cell,
|
||||||
@ -20,6 +21,8 @@ function EditCellPortal({
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const isDisabled = false;
|
const isDisabled = false;
|
||||||
|
|
||||||
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||||||
@ -68,6 +71,7 @@ function EditCellPortal({
|
|||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
tableId={tableKey}
|
tableId={tableKey}
|
||||||
|
table={table}
|
||||||
cellId={cell.row.id + '_' + cell.column.id}
|
cellId={cell.row.id + '_' + cell.column.id}
|
||||||
/>,
|
/>,
|
||||||
refTbody,
|
refTbody,
|
||||||
@ -112,6 +116,7 @@ export const getTableColumns = ({
|
|||||||
globalFilter={
|
globalFilter={
|
||||||
table.getState().globalFilter
|
table.getState().globalFilter
|
||||||
}
|
}
|
||||||
|
columnFilter={table.getState().columnFilters?.find(f => f.id === column.id)?.value}
|
||||||
isEditable={row.original?.row_type === 'INPUT' || false}
|
isEditable={row.original?.row_type === 'INPUT' || false}
|
||||||
backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
|
backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
|
||||||
color={columnColors[column.id]?.color_type?.['COLOR']}
|
color={columnColors[column.id]?.color_type?.['COLOR']}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { StageStatusChip } from './StageStatusChip';
|
|||||||
import { formatDateForApi } from './utils';
|
import { formatDateForApi } from './utils';
|
||||||
import CollapsibleTree from '../common/CollapsibleTree';
|
import CollapsibleTree from '../common/CollapsibleTree';
|
||||||
import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils';
|
import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils';
|
||||||
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants';
|
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants/constants';
|
||||||
|
|
||||||
export const AddStagesModal = ({
|
export const AddStagesModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Box, Divider, Stack } from '@mui/material';
|
import { Box, Divider, Stack } from '@mui/material';
|
||||||
import { UnLockedSvg } from '../common/icons/icons';
|
import { UnLockedSvg } from '../common/icons/icons';
|
||||||
import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants';
|
import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants/constants';
|
||||||
|
|
||||||
export const StageInfoForTooltip = ({ stageData = [] }) => {
|
export const StageInfoForTooltip = ({ stageData = [] }) => {
|
||||||
const accessData = stageData[0] || [];
|
const accessData = stageData[0] || [];
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
import { getStageStatus, getStatusColors } from './utils';
|
import { getStageStatus, getStatusColors } from './utils';
|
||||||
import { formatDate } from '../../../utils/formatDate';
|
import { formatDate } from '../../../utils/formatDate';
|
||||||
import { StageStatusChip } from '../StageStatusChip';
|
import { StageStatusChip } from '../StageStatusChip';
|
||||||
import { ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants';
|
import { ROLES_NAME_ID, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../constants/constants';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
|
|
||||||
export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
||||||
|
|||||||
33
web/src/components/common/NavigationToggle.jsx
Normal file
33
web/src/components/common/NavigationToggle.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// components/NavigationToggle.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
import { PrimaryOutlinedButton } from './Buttons/Buttons'; // Ваш компонент кнопки
|
||||||
|
|
||||||
|
const NavigationToggle = ({ currentView, onViewChange }) => {
|
||||||
|
const handleNavigateToTasks = () => {
|
||||||
|
onViewChange('/tasks');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToProjects = () => {
|
||||||
|
onViewChange('/projects');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
|
{currentView === 'tasks' ? (
|
||||||
|
<PrimaryOutlinedButton onClick={handleNavigateToProjects}>
|
||||||
|
← К проектам
|
||||||
|
</PrimaryOutlinedButton>
|
||||||
|
) : (
|
||||||
|
<PrimaryOutlinedButton onClick={handleNavigateToTasks}>
|
||||||
|
← К задачам
|
||||||
|
</PrimaryOutlinedButton>
|
||||||
|
)}
|
||||||
|
<Typography variant="h6" color="primary" sx={{ fontWeight: 200, fontSize: '1rem' }}>
|
||||||
|
{currentView === 'tasks' ? 'Управление задачами' : 'Управление проектами'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationToggle;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { Box, Paper, Typography } from '@mui/material';
|
import { Box, Paper, Typography } from '@mui/material';
|
||||||
import { CustomCheckbox } from '../../components/common/StyledCheckbox';
|
import { CustomCheckbox } from '../StyledCheckbox';
|
||||||
|
|
||||||
export const OrgUnitsAutocompletePaper = forwardRef(
|
export const OrgUnitsAutocompletePaper = forwardRef(
|
||||||
function OrgUnitsAutocompletePaper(
|
function OrgUnitsAutocompletePaper(
|
||||||
257
web/src/components/common/PaginatedList.jsx
Normal file
257
web/src/components/common/PaginatedList.jsx
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
// src/components/common/PaginatedList/PaginatedList.jsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { CardsSkeleton } from './Skeleton';
|
||||||
|
import {
|
||||||
|
ExportExitButton,
|
||||||
|
ExportWithTextButton,
|
||||||
|
} from './Buttons/ButtonsActions';
|
||||||
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Универсальный компонент для отображения списка с пагинацией и экспортом
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.items - Массив элементов для отображения
|
||||||
|
* @param {boolean} props.loading - Состояние загрузки
|
||||||
|
* @param {number} props.totalCount - Общее количество элементов
|
||||||
|
* @param {number} props.page - Текущая страница
|
||||||
|
* @param {number} props.limit - Количество элементов на странице
|
||||||
|
* @param {Function} props.onPageChange - Обработчик изменения страницы
|
||||||
|
* @param {Function} props.onLimitChange - Обработчик изменения лимита
|
||||||
|
* @param {Function} props.renderItem - Функция рендеринга элемента
|
||||||
|
* @param {Function} props.getItemId - Функция получения ID элемента
|
||||||
|
* @param {Function} props.getItemTitle - Функция получения заголовка элемента
|
||||||
|
* @param {string} props.entityName - Название сущности (для текстов)
|
||||||
|
* @param {string} props.emptyMessage - Сообщение при пустом списке
|
||||||
|
* @param {number} props.skeletonCount - Количество силуэтов таблиц при загрузки
|
||||||
|
* @param {Object} props.extraActions - Дополнительные действия справа
|
||||||
|
* @param {boolean} props.enableExport - Включить экспорт
|
||||||
|
* @param {Array} props.limitOptions - Опции для выбора лимита
|
||||||
|
* @param {number} props.maxHeight - Максимальная высота контейнера со скроллом
|
||||||
|
* @param {boolean} props.enableScroll - Включить скролл для списка
|
||||||
|
* @param {boolean} props.filterActions - Элементы фильтрации
|
||||||
|
*/
|
||||||
|
export function PaginatedList({
|
||||||
|
items = [],
|
||||||
|
loading = false,
|
||||||
|
totalCount = 0,
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
onPageChange,
|
||||||
|
onLimitChange,
|
||||||
|
renderItem,
|
||||||
|
getItemId,
|
||||||
|
getItemTitle = (item) => `Элемент ${getItemId(item)}`,
|
||||||
|
entityName = 'элементов',
|
||||||
|
emptyMessage = 'Элементы не найдены',
|
||||||
|
skeletonCount = 20,
|
||||||
|
extraActions = null,
|
||||||
|
enableExport = true,
|
||||||
|
limitOptions = [10, 20, 50, 100, 200],
|
||||||
|
maxHeight = 'calc(100vh - 300px)',
|
||||||
|
enableScroll = true,
|
||||||
|
filterActions = null,
|
||||||
|
}) {
|
||||||
|
const [exportMode, setExportMode] = useState(false);
|
||||||
|
const [exportList, setExportList] = useState([]);
|
||||||
|
|
||||||
|
// Сброс списка экспорта при переключении режима
|
||||||
|
useEffect(() => {
|
||||||
|
setExportList([]);
|
||||||
|
}, [exportMode]);
|
||||||
|
|
||||||
|
const handleAddToExport = (itemId, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setExportList((prev) => [...prev, itemId]);
|
||||||
|
} else {
|
||||||
|
setExportList((prev) => prev.filter((id) => id !== itemId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkExport = async () => {
|
||||||
|
await exportBulkForms(exportList);
|
||||||
|
setExportMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSingleExport = (itemId) => {
|
||||||
|
const item = items.find((item) => getItemId(item) === itemId);
|
||||||
|
const title = item ? getItemTitle(item) : `Элемент ${itemId}`;
|
||||||
|
exportSingleForm(itemId, title);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Если включен экспорт, передаем его пропсы в дочерние элементы
|
||||||
|
const renderItemWithExport = (item) => {
|
||||||
|
if (enableExport) {
|
||||||
|
return renderItem(item, {
|
||||||
|
exportMode,
|
||||||
|
onAddForExport: (checked) => {
|
||||||
|
handleAddToExport(getItemId(item), checked);
|
||||||
|
},
|
||||||
|
onExportClick: () => {
|
||||||
|
handleSingleExport(getItemId(item));
|
||||||
|
},
|
||||||
|
isSelected: exportList.includes(getItemId(item)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return renderItem(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
||||||
|
{/* Верхняя панель с экспортом и дополнительными действиями */}
|
||||||
|
{enableExport && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
{filterActions}
|
||||||
|
</div>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: '1.5rem' }}>
|
||||||
|
{exportMode && (
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Выделено {entityName}: {exportList.length}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Box sx={{ flex: 1 }} />
|
||||||
|
<Stack direction="row" spacing="0.5rem">
|
||||||
|
{!exportMode ? (
|
||||||
|
<>
|
||||||
|
<ExportWithTextButton
|
||||||
|
text="Пакетный экспорт"
|
||||||
|
onClick={() => setExportMode(true)}
|
||||||
|
height='2.5rem'
|
||||||
|
/>
|
||||||
|
{extraActions}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExportWithTextButton
|
||||||
|
text="Экспортировать"
|
||||||
|
onClick={handleBulkExport}
|
||||||
|
disabled={exportList.length === 0}
|
||||||
|
height='2.5rem'
|
||||||
|
/>
|
||||||
|
<ExportExitButton onClick={() => setExportMode(false)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Контейнер со скроллом для списка */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: enableScroll ? 'auto' : 'visible',
|
||||||
|
maxHeight: enableScroll ? maxHeight : 'none',
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
width: '8px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-track': {
|
||||||
|
background: '#f1f1f1',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: '#888',
|
||||||
|
borderRadius: '4px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:hover': {
|
||||||
|
background: '#555',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Список элементов */}
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
{loading ? (
|
||||||
|
<CardsSkeleton count={skeletonCount} />
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4, width: '100%' }}>
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
{emptyMessage}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
items.map((item) => renderItemWithExport(item))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Пагинация */}
|
||||||
|
{!loading && items.length > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
pt: 2,
|
||||||
|
pb: 2,
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
flexShrink: 0,
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Показать на странице:
|
||||||
|
</Typography>
|
||||||
|
<Select
|
||||||
|
value={limit}
|
||||||
|
onChange={onLimitChange}
|
||||||
|
size="small"
|
||||||
|
sx={{ minWidth: 70 }}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{limitOptions.map((option) => (
|
||||||
|
<MenuItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Всего: {totalCount}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
count={Math.ceil(totalCount / limit)}
|
||||||
|
page={page}
|
||||||
|
onChange={onPageChange}
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
disabled={loading}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<PaginationItem
|
||||||
|
{...item}
|
||||||
|
sx={{
|
||||||
|
'&.Mui-selected': {
|
||||||
|
backgroundColor: 'primary.main',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'primary.dark',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ width: 120 }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { EDIT_ACCESS_RUSSIAN, ROLES_ID_RUSSIAN_NAME } from '../../../constants';
|
import { EDIT_ACCESS_RUSSIAN, ROLES_ID_RUSSIAN_NAME } from '../../../constants/constants';
|
||||||
import {
|
import {
|
||||||
SelectWrapper,
|
SelectWrapper,
|
||||||
SelectContainer,
|
SelectContainer,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import { Switch, Buttons, StyledNavLink } from './SwitchFormTask.style';
|
import { Switch, Buttons, StyledNavLink } from './SwitchFormTask.style';
|
||||||
|
|
||||||
export function SwitchFormTask() {
|
export function SwitchFormTask() {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DIRECTION_TRANSLATE, FORM_TYPE_OPTIONS } from '../../../constants';
|
import { DIRECTION_TRANSLATE, FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
||||||
import {
|
import {
|
||||||
Section,
|
Section,
|
||||||
SectionStartPart,
|
SectionStartPart,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Box, Stack, Typography } from '@mui/material';
|
import { Box, Stack, Typography } from '@mui/material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import TableCard from '../TableCard/TableCard';
|
import TableCard from '../TableCard/TableCard';
|
||||||
@ -6,10 +6,10 @@ import { toast } from 'react-toastify';
|
|||||||
|
|
||||||
const TablesList = ({
|
const TablesList = ({
|
||||||
filteredTables,
|
filteredTables,
|
||||||
projects,
|
|
||||||
query,
|
query,
|
||||||
basePath = '/table',
|
basePath = '/table',
|
||||||
formInfo
|
formInfo,
|
||||||
|
isProject = false,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const TablesList = ({
|
|||||||
if (!formInfo){
|
if (!formInfo){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const path = `${basePath}/form/${formInfo.id}/form-type/${formInfo.form_type_code}/${table.sheet}/${table?.direction}`;
|
const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'PROJECT' : formInfo.form_type_code}/${table.sheet}/${table?.direction}/${table?.year}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<TableCard
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { formatDate } from '../../utils/formatDate';
|
|||||||
import { IconWithContent } from './IconWithContent';
|
import { IconWithContent } from './IconWithContent';
|
||||||
import { Chip } from '../styles/StyledChip';
|
import { Chip } from '../styles/StyledChip';
|
||||||
import { ExportButton } from '../common/Buttons/ButtonsActions';
|
import { ExportButton } from '../common/Buttons/ButtonsActions';
|
||||||
import { FORM_TYPE_TRANSLATE } from '../../constants';
|
import { FORM_TYPE_TRANSLATE } from '../../constants/constants';
|
||||||
|
|
||||||
const TaskCard = styled.div`
|
const TaskCard = styled.div`
|
||||||
width: 26.8rem;
|
width: 26.8rem;
|
||||||
|
|||||||
90
web/src/constants/projectConfig.jsx
Normal file
90
web/src/constants/projectConfig.jsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
export const DEFAULT_PROJECT_CONFIG = {
|
||||||
|
'ССП/РФ': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: [],
|
||||||
|
placeholder: 'Выберите ССП/РФ',
|
||||||
|
defaultValue: null,
|
||||||
|
required_field: true,
|
||||||
|
},
|
||||||
|
'Год': {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Введите год',
|
||||||
|
defaultValue: null,
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
required_field: false,
|
||||||
|
},
|
||||||
|
'Тип проекта развития': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: [
|
||||||
|
'Открытие ВСП',
|
||||||
|
'Закрытие ВСП',
|
||||||
|
'Переезд ВСП',
|
||||||
|
'Реновация РФ',
|
||||||
|
'Открытие УРМ',
|
||||||
|
],
|
||||||
|
placeholder: 'Выберите тип проекта',
|
||||||
|
defaultValue: null,
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
'Название проекта': {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Введите название проекта',
|
||||||
|
defaultValue: null,
|
||||||
|
required_field: true,
|
||||||
|
},
|
||||||
|
'Формат ВСП': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: [
|
||||||
|
'Фланговый',
|
||||||
|
'Типовой',
|
||||||
|
'Розничный',
|
||||||
|
'МСБ',
|
||||||
|
'Лёгкий',
|
||||||
|
'Мини',
|
||||||
|
'Розничный-киоск',
|
||||||
|
'МБО',
|
||||||
|
'Офис самообслуживания',
|
||||||
|
'Другое',
|
||||||
|
],
|
||||||
|
placeholder: 'Выберите формат ВСП',
|
||||||
|
defaultValue: null,
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
'Адрес объекта': {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Введите адрес объекта',
|
||||||
|
defaultValue: '',
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
'Целевая численность, чел.': {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Введите число',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
'Тип размещения': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: ['Собственность', 'Аренда', 'Субаренда'],
|
||||||
|
placeholder: 'Выберите тип размещения',
|
||||||
|
defaultValue: null,
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
'Площадь размещения, м2': {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Введите число',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
required_field: false,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { PageContainer } from '../../StyledForPage';
|
import { PageContainer } from '../../StyledForPage';
|
||||||
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch.jsx';
|
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch.jsx';
|
||||||
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Box, Typography, Link, Chip } from '@mui/material';
|
import { Box, Typography, Link, Chip } from '@mui/material';
|
||||||
import { ROLES_ID_RUSSIAN_NAME } from '../../../../constants';
|
import { ROLES_ID_RUSSIAN_NAME } from '../../../../constants/constants';
|
||||||
import {
|
import {
|
||||||
ENTITY_RUSSIAN_NAMES,
|
ENTITY_RUSSIAN_NAMES,
|
||||||
getRussianNamesForType,
|
getRussianNamesForType,
|
||||||
|
|||||||
@ -12,8 +12,8 @@ export const EVENT_CONFIGS = {
|
|||||||
format: 'Утвержденный формат',
|
format: 'Утвержденный формат',
|
||||||
opened_at: 'Дата открытия ВСП',
|
opened_at: 'Дата открытия ВСП',
|
||||||
notes: 'Примечания',
|
notes: 'Примечания',
|
||||||
location_form: 'Форма расположения ВСП',
|
placement_type: 'Форма расположения ВСП',
|
||||||
numbers: 'Штатная численность ВСП',
|
staff_count: 'Штатная численность ВСП',
|
||||||
total_area: 'Площадь помещений ВСП',
|
total_area: 'Площадь помещений ВСП',
|
||||||
rent_contract_num: 'Номер договора аренды',
|
rent_contract_num: 'Номер договора аренды',
|
||||||
rent_end_date: 'Срок окончания договора аренды',
|
rent_end_date: 'Срок окончания договора аренды',
|
||||||
@ -21,8 +21,6 @@ export const EVENT_CONFIGS = {
|
|||||||
branch_name: 'Региональный филиал ',
|
branch_name: 'Региональный филиал ',
|
||||||
closed_at: 'Дата закрытия ВСП',
|
closed_at: 'Дата закрытия ВСП',
|
||||||
user_email: 'Почта пользователя',
|
user_email: 'Почта пользователя',
|
||||||
staff_count: 'Кол-во работников',
|
|
||||||
placement_type: 'Тип размещение',
|
|
||||||
is_active: 'Активный',
|
is_active: 'Активный',
|
||||||
},
|
},
|
||||||
VSP_UPDATE: {
|
VSP_UPDATE: {
|
||||||
@ -32,8 +30,8 @@ export const EVENT_CONFIGS = {
|
|||||||
format: 'Утвержденный формат',
|
format: 'Утвержденный формат',
|
||||||
opened_at: 'Дата открытия ВСП',
|
opened_at: 'Дата открытия ВСП',
|
||||||
notes: 'Примечания',
|
notes: 'Примечания',
|
||||||
location_form: 'Форма расположения ВСП',
|
placement_type: 'Форма расположения ВСП',
|
||||||
numbers: 'Штатная численность ВСП',
|
staff_count: 'Штатная численность ВСП',
|
||||||
total_area: 'Площадь помещений ВСП',
|
total_area: 'Площадь помещений ВСП',
|
||||||
rent_contract_num: 'Номер договора аренды',
|
rent_contract_num: 'Номер договора аренды',
|
||||||
rent_end_date: 'Срок окончания договора аренды',
|
rent_end_date: 'Срок окончания договора аренды',
|
||||||
@ -41,8 +39,6 @@ export const EVENT_CONFIGS = {
|
|||||||
'changes.branch_name': 'Региональный филиал ',
|
'changes.branch_name': 'Региональный филиал ',
|
||||||
closed_at: 'Дата закрытия ВСП',
|
closed_at: 'Дата закрытия ВСП',
|
||||||
user_email: 'Почта пользователя',
|
user_email: 'Почта пользователя',
|
||||||
staff_count: 'Кол-во работников',
|
|
||||||
placement_type: 'Тип размещение',
|
|
||||||
is_active: 'Активный',
|
is_active: 'Активный',
|
||||||
},
|
},
|
||||||
USER_ACCESS_GRANTED: {
|
USER_ACCESS_GRANTED: {
|
||||||
@ -82,15 +78,15 @@ export const EVENT_CONFIGS = {
|
|||||||
},
|
},
|
||||||
ACCESS_EXTEND: {
|
ACCESS_EXTEND: {
|
||||||
form_id: 'Информация о задаче',
|
form_id: 'Информация о задаче',
|
||||||
closes_at_after: 'Дата закрытия',
|
closes_at_after: 'Дата закрытия (новая)',
|
||||||
closes_at_before: 'Дата открытия',
|
closes_at_before: 'Дата закрытия (старая)',
|
||||||
phase_name: 'Названия этапа',
|
phase_name: 'Названия этапа',
|
||||||
sheet: "Лист",
|
sheet: "Лист",
|
||||||
},
|
},
|
||||||
ACCESS_WINDOW_CHANGE: {
|
ACCESS_WINDOW_CHANGE: {
|
||||||
form_id: 'Информация о задаче',
|
form_id: 'Информация о задаче',
|
||||||
closes_at: 'Дата закрытия',
|
closes_at: 'Дата закрытия',
|
||||||
open_at: 'Дата открытия',
|
opens_at: 'Дата открытия',
|
||||||
phase_name: 'Названия этапа',
|
phase_name: 'Названия этапа',
|
||||||
role: 'Для роли',
|
role: 'Для роли',
|
||||||
column_keys: 'Колонки в этапе',
|
column_keys: 'Колонки в этапе',
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { FormsApi } from '../../../../api/forms';
|
|||||||
import { SspApi } from '../../../../api/ssp';
|
import { SspApi } from '../../../../api/ssp';
|
||||||
import { TasksApi } from '../../../../api/tasks';
|
import { TasksApi } from '../../../../api/tasks';
|
||||||
import { UsersApi } from '../../../../api/users';
|
import { UsersApi } from '../../../../api/users';
|
||||||
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, ROLES_ID_RUSSIAN_NAME, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../../constants';
|
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, ROLES_ID_RUSSIAN_NAME, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../../../constants/constants';
|
||||||
|
|
||||||
export const VALUE_TRANSFORMERS = {
|
export const VALUE_TRANSFORMERS = {
|
||||||
Роль: transformRole,
|
Роль: transformRole,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { PageContainer } from '../../StyledForPage';
|
import { PageContainer } from '../../StyledForPage';
|
||||||
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import Modal from '../../../components/common/Modal/Modal';
|
import Modal from '../../../components/common/Modal/Modal';
|
||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
import SelectWithOptions from '../components/SelectWithOptions';
|
import SelectWithOptions from '../components/SelectWithOptions';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import { AutocompleteForChangeOptions } from '../components/AutocompleteForChangeOptions';
|
import { AutocompleteForChangeOptions } from '../components/AutocompleteForChangeOptions';
|
||||||
|
|
||||||
export const ModalEditUserSsp = ({
|
export const ModalEditUserSsp = ({
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
|||||||
import {
|
import {
|
||||||
ROLES_ID_RUSSIAN_NAME_ARRAY as ROLES,
|
ROLES_ID_RUSSIAN_NAME_ARRAY as ROLES,
|
||||||
ROLES_ID_RUSSIAN_NAME,
|
ROLES_ID_RUSSIAN_NAME,
|
||||||
} from '../../../../constants';
|
} from '../../../../constants/constants';
|
||||||
import SelectWithOptions from '../../components/SelectWithOptions';
|
import SelectWithOptions from '../../components/SelectWithOptions';
|
||||||
import useAbbreviation from './hooks/useAbbreviation';
|
import useAbbreviation from './hooks/useAbbreviation';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import {
|
|||||||
CheckedIcon,
|
CheckedIcon,
|
||||||
IndeterminateIcon,
|
IndeterminateIcon,
|
||||||
} from '../../IconsForTable/icons';
|
} from '../../IconsForTable/icons';
|
||||||
import { ROLES_NAME_ID } from '../../../../constants';
|
import { ROLES_NAME_ID } from '../../../../constants/constants';
|
||||||
import { createFilterData } from '../../utils/filters';
|
import { createFilterData } from '../../utils/filters';
|
||||||
import {
|
import {
|
||||||
ROLES_ID_RUSSIAN_NAME_ARRAY as ROLES,
|
ROLES_ID_RUSSIAN_NAME_ARRAY as ROLES,
|
||||||
ROLES_ID_NAME,
|
ROLES_ID_NAME,
|
||||||
} from '../../../../constants';
|
} from '../../../../constants/constants';
|
||||||
import SelectWithOptions from '../../components/SelectWithOptions';
|
import SelectWithOptions from '../../components/SelectWithOptions';
|
||||||
import RowActionsMenu from './RowActionMenu';
|
import RowActionsMenu from './RowActionMenu';
|
||||||
import DepartmentCell from './DepartmentCell';
|
import DepartmentCell from './DepartmentCell';
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { CheckIcon, CheckedIcon } from '../IconsForTable/icons';
|
import { CheckIcon, CheckedIcon } from '../IconsForTable/icons';
|
||||||
import { TrashSvg } from '../../../components/common/icons/icons';
|
import { TrashSvg } from '../../../components/common/icons/icons';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
|
|
||||||
const StyleForChecked = {
|
const StyleForChecked = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Button, Stack } from '@mui/material';
|
|||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { ROLES_ID_RUSSIAN_NAME } from '../../../constants';
|
import { ROLES_ID_RUSSIAN_NAME } from '../../../constants/constants';
|
||||||
import { UserIcon, TrashSvg } from '../../../components/common/icons/icons';
|
import { UserIcon, TrashSvg } from '../../../components/common/icons/icons';
|
||||||
import {
|
import {
|
||||||
DangerOutlinedButton,
|
DangerOutlinedButton,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { PageContainer } from '../../StyledForPage';
|
import { PageContainer } from '../../StyledForPage';
|
||||||
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp';
|
||||||
import { ROLES_ID_RUSSIAN_NAME_ARRAY, ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_ID_RUSSIAN_NAME_ARRAY, ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import { Box, TextField, Autocomplete, FormLabel } from '@mui/material';
|
import { Box, TextField, Autocomplete, FormLabel } from '@mui/material';
|
||||||
import SearchComponent from '../../../components/common/SearchComponent';
|
import SearchComponent from '../../../components/common/SearchComponent';
|
||||||
@ -10,7 +10,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { SspApi } from '../../../api/ssp';
|
import { SspApi } from '../../../api/ssp';
|
||||||
import { UsersApi } from '../../../api/users';
|
import { UsersApi } from '../../../api/users';
|
||||||
import UserTable from './UserTable/UserTable';
|
import UserTable from './UserTable/UserTable';
|
||||||
import { ROLES_ID_RUSSIAN_NAME } from '../../../constants';
|
import { ROLES_ID_RUSSIAN_NAME } from '../../../constants/constants';
|
||||||
import { ModalEditUserSsp } from './ModalEditUserSsp';
|
import { ModalEditUserSsp } from './ModalEditUserSsp';
|
||||||
import { ModalAddUsers } from './ModalAddUsers';
|
import { ModalAddUsers } from './ModalAddUsers';
|
||||||
import UsersActionBar from './UsersActionBar';
|
import UsersActionBar from './UsersActionBar';
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import TableDictVsp from './components/TableDictVsp';
|
|||||||
import { PageContainer } from '../StyledForPage';
|
import { PageContainer } from '../StyledForPage';
|
||||||
import { SspApi } from '../../api/ssp';
|
import { SspApi } from '../../api/ssp';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../constants';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
import DictVspFilters from './components/DictVspFilters';
|
import DictVspFilters from './components/DictVspFilters';
|
||||||
|
|
||||||
const DictVspInfo = () => {
|
const DictVspInfo = () => {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
} from '../../../../components/common/Buttons/Buttons';
|
} from '../../../../components/common/Buttons/Buttons';
|
||||||
import { useAuth } from '../../../../app/context/AuthProvider';
|
import { useAuth } from '../../../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../../../constants';
|
import { ROLES_NAME_ID } from '../../../../constants/constants';
|
||||||
import {
|
import {
|
||||||
ModalContainer,
|
ModalContainer,
|
||||||
FieldContainer,
|
FieldContainer,
|
||||||
@ -43,8 +43,8 @@ const ModalEditAddVsp = ({
|
|||||||
open_date: null,
|
open_date: null,
|
||||||
close_date: null,
|
close_date: null,
|
||||||
notes: '',
|
notes: '',
|
||||||
location_form: '',
|
placement_type: '',
|
||||||
numbers: '',
|
staff_count: '',
|
||||||
area: null,
|
area: null,
|
||||||
rent_contract_num: '',
|
rent_contract_num: '',
|
||||||
rent_end_date: null,
|
rent_end_date: null,
|
||||||
@ -94,8 +94,8 @@ const ModalEditAddVsp = ({
|
|||||||
? new Date(initialData.close_date)
|
? new Date(initialData.close_date)
|
||||||
: null,
|
: null,
|
||||||
notes: initialData.notes || '',
|
notes: initialData.notes || '',
|
||||||
location_form: initialData.location_form || '',
|
placement_type: initialData.placement_type || '',
|
||||||
numbers: initialData.numbers?.toString() || '',
|
staff_count: initialData.staff_count?.toString() || '',
|
||||||
area: initialData.area || null,
|
area: initialData.area || null,
|
||||||
rent_contract_num: initialData.rent_contract_num || '',
|
rent_contract_num: initialData.rent_contract_num || '',
|
||||||
rent_end_date: initialData.rent_end_date
|
rent_end_date: initialData.rent_end_date
|
||||||
@ -114,8 +114,8 @@ const ModalEditAddVsp = ({
|
|||||||
open_date: null,
|
open_date: null,
|
||||||
close_date: null,
|
close_date: null,
|
||||||
notes: '',
|
notes: '',
|
||||||
location_form: '',
|
placement_type: '',
|
||||||
numbers: '',
|
staff_count: '',
|
||||||
area: null,
|
area: null,
|
||||||
rent_contract_num: '',
|
rent_contract_num: '',
|
||||||
rent_end_date: null,
|
rent_end_date: null,
|
||||||
@ -215,7 +215,7 @@ const ModalEditAddVsp = ({
|
|||||||
|
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...formData,
|
...formData,
|
||||||
numbers: formData.numbers ? parseInt(formData.numbers, 10) : null,
|
staff_count: formData.staff_count ? parseInt(formData.staff_count, 10) : null,
|
||||||
area: formData.area ? parseFloat(formData.area) : null,
|
area: formData.area ? parseFloat(formData.area) : null,
|
||||||
open_date: formData.open_date
|
open_date: formData.open_date
|
||||||
? formData.open_date.toLocaleDateString('en-CA')
|
? formData.open_date.toLocaleDateString('en-CA')
|
||||||
@ -255,8 +255,8 @@ const ModalEditAddVsp = ({
|
|||||||
open_date: null,
|
open_date: null,
|
||||||
close_date: null,
|
close_date: null,
|
||||||
notes: '',
|
notes: '',
|
||||||
location_form: '',
|
placement_type: '',
|
||||||
numbers: '',
|
staff_count: '',
|
||||||
area: '',
|
area: '',
|
||||||
rent_contract_num: '',
|
rent_contract_num: '',
|
||||||
rent_end_date: null,
|
rent_end_date: null,
|
||||||
@ -461,11 +461,11 @@ const ModalEditAddVsp = ({
|
|||||||
<FieldLabel>Форма расположения ВСП</FieldLabel>
|
<FieldLabel>Форма расположения ВСП</FieldLabel>
|
||||||
<StyledTextField
|
<StyledTextField
|
||||||
select
|
select
|
||||||
value={formData.location_form}
|
value={formData.placement_type}
|
||||||
onChange={handleChange('location_form')}
|
onChange={handleChange('placement_type')}
|
||||||
placeholder="Выберите форму расположения"
|
placeholder="Выберите форму расположения"
|
||||||
error={!!errors.location_form}
|
error={!!errors.placement_type}
|
||||||
helperText={errors.location_form}
|
helperText={errors.placement_type}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
select: {
|
select: {
|
||||||
@ -495,11 +495,11 @@ const ModalEditAddVsp = ({
|
|||||||
<FieldLabel>Штатная численность ВСП</FieldLabel>
|
<FieldLabel>Штатная численность ВСП</FieldLabel>
|
||||||
<StyledTextField
|
<StyledTextField
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.numbers}
|
value={formData.staff_count}
|
||||||
onChange={handleNumberChange('numbers')}
|
onChange={handleNumberChange('staff_count')}
|
||||||
placeholder="Введите численность"
|
placeholder="Введите численность"
|
||||||
error={!!errors.numbers}
|
error={!!errors.staff_count}
|
||||||
helperText={errors.numbers}
|
helperText={errors.staff_count}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: { min: 1, step: 1 },
|
htmlInput: { min: 1, step: 1 },
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
PrimaryOutlinedButton,
|
PrimaryOutlinedButton,
|
||||||
} from '../../../components/common/Buttons/Buttons';
|
} from '../../../components/common/Buttons/Buttons';
|
||||||
import { useAuth } from '../../../app/context/AuthProvider';
|
import { useAuth } from '../../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../../constants';
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
||||||
import ModalExport from './Modals/ModalExport';
|
import ModalExport from './Modals/ModalExport';
|
||||||
|
|
||||||
const TableDictVsp = ({
|
const TableDictVsp = ({
|
||||||
@ -192,7 +192,7 @@ const TableDictVsp = ({
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location_form',
|
accessorKey: 'placement_type',
|
||||||
header: 'Форма расположения ВСП',
|
header: 'Форма расположения ВСП',
|
||||||
size: 170,
|
size: 170,
|
||||||
minSize: 140,
|
minSize: 140,
|
||||||
@ -204,7 +204,7 @@ const TableDictVsp = ({
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'numbers',
|
accessorKey: 'staff_count',
|
||||||
header: 'Штатная численность ВСП',
|
header: 'Штатная численность ВСП',
|
||||||
size: 170,
|
size: 170,
|
||||||
minSize: 140,
|
minSize: 140,
|
||||||
|
|||||||
@ -8,12 +8,13 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
||||||
import { useAuth } from '../app/context/AuthProvider';
|
import { useAuth } from '../app/context/AuthProvider';
|
||||||
import { BackButton } from '../components/common/Buttons/BackButton';
|
import { BackButton } from '../components/common/Buttons/BackButton';
|
||||||
import { SHEET_NAME } from '../constants';
|
import { SHEET_NAME } from '../constants/constants';
|
||||||
|
|
||||||
const TableInfo = ({ formId, sheetName }) => {
|
const TableInfo = ({ formId, sheetName, isProject = false }) => {
|
||||||
|
const path = (isProject ? `/project/` : '/task/') + formId;
|
||||||
const portalContent = (
|
const portalContent = (
|
||||||
<TaskInfoContainer>
|
<TaskInfoContainer>
|
||||||
<BackButton to={`/task/${formId}`} />
|
<BackButton to={path} />
|
||||||
{sheetName && (
|
{sheetName && (
|
||||||
<NameTask>{SHEET_NAME[sheetName] || sheetName}</NameTask>
|
<NameTask>{SHEET_NAME[sheetName] || sheetName}</NameTask>
|
||||||
)}
|
)}
|
||||||
@ -28,20 +29,29 @@ const TableInfo = ({ formId, sheetName }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function NewTablePage() {
|
export default function NewTablePage() {
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction, year } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const isProject = formType == 'PROJECT';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{formId && <TableInfo formId={formId} sheetName={sheetName} />}
|
{formId && <TableInfo formId={formId} sheetName={sheetName} isProject={isProject} />}
|
||||||
{user && (
|
{user && (
|
||||||
<RealtimeProvider
|
<RealtimeProvider
|
||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
direction={direction === 'null' ? null : direction}
|
direction={direction === 'null' ? null : direction}
|
||||||
|
isProject={isProject}
|
||||||
|
year={year}
|
||||||
>
|
>
|
||||||
<RealtimeTable formType={formType} formId={formId} sheetName={sheetName} direction={direction === 'null' ? null : direction} />
|
<RealtimeTable
|
||||||
|
formType={formType}
|
||||||
|
formId={formId}
|
||||||
|
sheetName={sheetName}
|
||||||
|
direction={direction === 'null' ? null : direction}
|
||||||
|
year={year}
|
||||||
|
/>
|
||||||
</RealtimeProvider>
|
</RealtimeProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Box, Button, Divider, TextField, Typography } from '@mui/material';
|
import { Box, Button, Divider, TextField, Typography } from '@mui/material';
|
||||||
import SelectWithOptions from '../AdminPanel/components/SelectWithOptions';
|
import SelectWithOptions from '../AdminPanel/components/SelectWithOptions';
|
||||||
import Modal from '../../components/common/Modal/Modal';
|
import Modal from '../../components/common/Modal/Modal';
|
||||||
@ -9,25 +9,98 @@ export const ModalCreateProject = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onCreate,
|
onCreate,
|
||||||
config = {},
|
config = {},
|
||||||
|
initialData = null,
|
||||||
|
title = 'Создание проекта',
|
||||||
|
buttonText = 'Создать',
|
||||||
}) => {
|
}) => {
|
||||||
const [projectData, setProjectData] = useState(() => {
|
const [projectData, setProjectData] = useState(() => {
|
||||||
const initialData = {};
|
if (initialData) {
|
||||||
|
return { ...initialData };
|
||||||
|
}
|
||||||
|
// Иначе создаем из конфига
|
||||||
|
const data = {};
|
||||||
Object.keys(config).forEach((key) => {
|
Object.keys(config).forEach((key) => {
|
||||||
initialData[key] = config[key].defaultValue;
|
data[key] = config[key].defaultValue;
|
||||||
});
|
});
|
||||||
return initialData;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
setProjectData({ ...initialData });
|
||||||
|
} else {
|
||||||
|
const data = {};
|
||||||
|
Object.keys(config).forEach((key) => {
|
||||||
|
data[key] = config[key].defaultValue;
|
||||||
|
});
|
||||||
|
setProjectData(data);
|
||||||
|
}
|
||||||
|
setErrors({});
|
||||||
|
}, [initialData, config, open]);
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setProjectData((prev) => ({
|
setProjectData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateField = (fieldName, config) => {
|
||||||
|
if (config.required_field) {
|
||||||
|
const value = projectData[fieldName];
|
||||||
|
|
||||||
|
if (config.type === 'multiselect') {
|
||||||
|
if (!value || (Array.isArray(value) && value.length === 0) || value === '') {
|
||||||
|
return `Поле "${fieldName}" обязательно для заполнения`;
|
||||||
|
}
|
||||||
|
} else if (config.type === 'text') {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return `Поле "${fieldName}" обязательно для заполнения`;
|
||||||
|
}
|
||||||
|
} else if (config.type === 'number') {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return `Поле "${fieldName}" обязательно для заполнения`;
|
||||||
|
}
|
||||||
|
if (config.min !== undefined && Number(value) < config.min) {
|
||||||
|
return `Значение должно быть не меньше ${config.min}`;
|
||||||
|
}
|
||||||
|
if (config.max !== undefined && Number(value) > config.max) {
|
||||||
|
return `Значение должно быть не больше ${config.max}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAllFields = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
Object.entries(config).forEach(([fieldName, fieldConfig]) => {
|
||||||
|
const error = validateField(fieldName, fieldConfig);
|
||||||
|
if (error) {
|
||||||
|
newErrors[fieldName] = error;
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return !hasErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickCreate = () => {
|
const handleClickCreate = () => {
|
||||||
onCreate(projectData);
|
if (validateAllFields()) {
|
||||||
onClose();
|
onCreate(projectData);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -36,10 +109,14 @@ export const ModalCreateProject = ({
|
|||||||
resetData[key] = config[key].defaultValue;
|
resetData[key] = config[key].defaultValue;
|
||||||
});
|
});
|
||||||
setProjectData(resetData);
|
setProjectData(resetData);
|
||||||
|
setErrors({});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderField = (fieldName, config) => {
|
const renderField = (fieldName, config) => {
|
||||||
|
const hasError = !!errors[fieldName];
|
||||||
|
const errorText = errors[fieldName];
|
||||||
|
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case 'multiselect':
|
case 'multiselect':
|
||||||
return (
|
return (
|
||||||
@ -50,9 +127,13 @@ export const ModalCreateProject = ({
|
|||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
paddingBottom: '.25rem',
|
paddingBottom: '.25rem',
|
||||||
|
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldName}
|
{fieldName}
|
||||||
|
{config.required_field && (
|
||||||
|
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<SelectWithOptions
|
<SelectWithOptions
|
||||||
className="sameSize"
|
className="sameSize"
|
||||||
@ -61,7 +142,22 @@ export const ModalCreateProject = ({
|
|||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
onChange={(value) => handleChange(fieldName, value)}
|
onChange={(value) => handleChange(fieldName, value)}
|
||||||
width="28.25rem"
|
width="28.25rem"
|
||||||
|
style={{
|
||||||
|
borderColor: hasError ? 'var(--error-color, #d32f2f)' : undefined,
|
||||||
|
borderWidth: hasError ? '2px' : undefined,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{hasError && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: 'var(--error-color, #d32f2f)',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
marginTop: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorText}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,11 +169,14 @@ export const ModalCreateProject = ({
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
color: 'var(--grey-dark)',
|
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||||||
paddingBottom: '.25rem',
|
paddingBottom: '.25rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldName}
|
{fieldName}
|
||||||
|
{config.required_field && (
|
||||||
|
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
className="sameSize"
|
className="sameSize"
|
||||||
@ -85,6 +184,14 @@ export const ModalCreateProject = ({
|
|||||||
onChange={(e) => handleChange(fieldName, e.target.value)}
|
onChange={(e) => handleChange(fieldName, e.target.value)}
|
||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
size="small"
|
size="small"
|
||||||
|
error={hasError}
|
||||||
|
helperText={hasError ? errorText : ''}
|
||||||
|
FormHelperTextProps={{
|
||||||
|
sx: {
|
||||||
|
marginLeft: 0,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@ -97,11 +204,14 @@ export const ModalCreateProject = ({
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
color: 'var(--grey-dark)',
|
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||||||
paddingBottom: '.25rem',
|
paddingBottom: '.25rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldName}
|
{fieldName}
|
||||||
|
{config.required_field && (
|
||||||
|
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
className="sameSize"
|
className="sameSize"
|
||||||
@ -109,7 +219,6 @@ export const ModalCreateProject = ({
|
|||||||
value={projectData[fieldName] || ''}
|
value={projectData[fieldName] || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const inputValue = e.target.value;
|
const inputValue = e.target.value;
|
||||||
//только числа
|
|
||||||
const isValidInput =
|
const isValidInput =
|
||||||
/^\d*\.?\d*$/.test(inputValue) || inputValue === '';
|
/^\d*\.?\d*$/.test(inputValue) || inputValue === '';
|
||||||
if (isValidInput) {
|
if (isValidInput) {
|
||||||
@ -118,6 +227,8 @@ export const ModalCreateProject = ({
|
|||||||
}}
|
}}
|
||||||
placeholder={config.placeholder}
|
placeholder={config.placeholder}
|
||||||
size="small"
|
size="small"
|
||||||
|
error={hasError}
|
||||||
|
helperText={hasError ? errorText : ''}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
min: config.min,
|
min: config.min,
|
||||||
@ -125,6 +236,12 @@ export const ModalCreateProject = ({
|
|||||||
step: config.step || 1,
|
step: config.step || 1,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
FormHelperTextProps={{
|
||||||
|
sx: {
|
||||||
|
marginLeft: 0,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@ -136,7 +253,7 @@ export const ModalCreateProject = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={'Создание проекта'}
|
title={title}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
customSize={'43.75'}
|
customSize={'43.75'}
|
||||||
@ -154,7 +271,7 @@ export const ModalCreateProject = ({
|
|||||||
onClick={handleClickCreate}
|
onClick={handleClickCreate}
|
||||||
style={{ height: '2.5rem' }}
|
style={{ height: '2.5rem' }}
|
||||||
>
|
>
|
||||||
Создать
|
{buttonText}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
349
web/src/pages/ProjectsPage/ProjectCard.jsx
Normal file
349
web/src/pages/ProjectsPage/ProjectCard.jsx
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
// src/components/common/ProjectCardComponent/ProjectCardComponent.jsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ProjectsApi } from '../../api/projects';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import {
|
||||||
|
CheckBoxCheckSvg,
|
||||||
|
CheckBoxUncheckSvg,
|
||||||
|
DateIcon,
|
||||||
|
Edit,
|
||||||
|
ExportSvg,
|
||||||
|
UserIcon,
|
||||||
|
BuildingIcon,
|
||||||
|
LocationIcon,
|
||||||
|
} from '../../components/common/icons/icons';
|
||||||
|
import { IconWithContent } from '../../components/common/IconWithContent';
|
||||||
|
import { ExportButton } from '../../components/common/Buttons/ButtonsActions';
|
||||||
|
import { ModalCreateProject } from './ModalCreateProject';
|
||||||
|
|
||||||
|
const ProjectCard = styled.div`
|
||||||
|
width: 26.8rem;
|
||||||
|
height: 15rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 1.25rem;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0.06rem solid var(--Stroke, rgba(0, 0, 0, 0.1));
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow:
|
||||||
|
0rem 2rem 4rem -2rem rgba(0, 0, 0, 0.02),
|
||||||
|
0rem 4rem 6rem -1rem rgba(0, 0, 0, 0.02);
|
||||||
|
background: var(--White, rgba(255, 255, 255, 1));
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardHeader = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectId = styled.div`
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1rem;
|
||||||
|
letter-spacing: 0rem;
|
||||||
|
text-align: left;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardBody = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8125rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectTitle = styled.div`
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--Text-Primary, #333);
|
||||||
|
line-height: 1.63;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatusChip = styled(Chip)`
|
||||||
|
font-size: 0.7rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background-color: ${({ status }) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return '#4caf50';
|
||||||
|
case 'completed': return '#2196f3';
|
||||||
|
case 'paused': return '#ff9800';
|
||||||
|
case 'archived': return '#9e9e9e';
|
||||||
|
default: return '#9e9e9e';
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
color: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LevelChip = styled(Chip)`
|
||||||
|
font-size: 0.6rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectInfoGrid = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InfoChip = styled(Chip)`
|
||||||
|
font-size: 0.6rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
active: 'Активный',
|
||||||
|
completed: 'Завершен',
|
||||||
|
paused: 'Приостановлен',
|
||||||
|
archived: 'Архивирован',
|
||||||
|
};
|
||||||
|
return statusMap[status] || status || 'Не указан';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectCardComponent = ({
|
||||||
|
project,
|
||||||
|
projectDataInit,
|
||||||
|
exportMode = false,
|
||||||
|
isLoadingFile = false,
|
||||||
|
onAddForExport,
|
||||||
|
onExportClick,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
org_unit_name,
|
||||||
|
years,
|
||||||
|
status,
|
||||||
|
level,
|
||||||
|
project_type,
|
||||||
|
vsp_format,
|
||||||
|
placement_type,
|
||||||
|
object_address,
|
||||||
|
staff_count,
|
||||||
|
total_area,
|
||||||
|
report_count,
|
||||||
|
parent_id,
|
||||||
|
} = project;
|
||||||
|
|
||||||
|
const [isEditModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [currentName, setCurrentName] = useState(name || '');
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
const [editFormData, setEditFormData] = useState({});
|
||||||
|
const [projectData, setProjectData] = useState(projectDataInit);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsChecked(false);
|
||||||
|
}, [exportMode]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Функция для подготовки данных проекта к редактированию
|
||||||
|
const prepareProjectDataForEdit = () => {
|
||||||
|
return {
|
||||||
|
'ССП/РФ': project.org_unit_id || null,
|
||||||
|
'Год': years || null,
|
||||||
|
'Тип проекта развития': project_type || null,
|
||||||
|
'Название проекта': currentName,
|
||||||
|
'Формат ВСП': vsp_format || null,
|
||||||
|
'Адрес объекта': object_address || '',
|
||||||
|
'Целевая численность, чел.': staff_count || 0,
|
||||||
|
'Тип размещения': placement_type || null,
|
||||||
|
'Площадь размещения, м2': total_area || 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEdit = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setEditFormData(prepareProjectDataForEdit());
|
||||||
|
setEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseEdit = () => {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
setEditFormData({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!isLoadingFile) {
|
||||||
|
setIsChecked(!isChecked);
|
||||||
|
onAddForExport(!isChecked);
|
||||||
|
} else {
|
||||||
|
toast.warning('Файл формируется!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportProject = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onExportClick?.(id, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigate = () => {
|
||||||
|
if (!exportMode) {
|
||||||
|
navigate(`/project/${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновленная функция обновления проекта
|
||||||
|
const updateProject = async (formData) => {
|
||||||
|
try {
|
||||||
|
const projectData = {
|
||||||
|
name: formData['Название проекта'] || currentName,
|
||||||
|
project_type: formData['Тип проекта развития'] || null,
|
||||||
|
vsp_format: formData['Формат ВСП'] || null,
|
||||||
|
object_address: formData['Адрес объекта'] || '',
|
||||||
|
staff_count: Number(formData['Целевая численность, чел.']) || 0,
|
||||||
|
placement_type: formData['Тип размещения'] || null,
|
||||||
|
total_area: Number(formData['Площадь размещения, м2']) || 0,
|
||||||
|
year: formData['Год'] ? Number(formData['Год']) : null,
|
||||||
|
branch_id: formData['ССП/РФ'] || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ProjectsApi.update(id, projectData);
|
||||||
|
|
||||||
|
// Обновляем локальные данные
|
||||||
|
setCurrentName(projectData.name);
|
||||||
|
setEditModalOpen(false);
|
||||||
|
toast.success('Данные проекта успешно обновлены');
|
||||||
|
|
||||||
|
// Можно добавить рефреш данных или перезагрузку страницы
|
||||||
|
window.location.reload(); // или используйте callback для обновления списка
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error?.response?.data?.detail || 'Не удалось обновить данные проекта',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProject = async (formData) => {
|
||||||
|
await updateProject(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (name) => {
|
||||||
|
if (!name) return '?';
|
||||||
|
const words = name.split(' ');
|
||||||
|
if (words.length >= 2) {
|
||||||
|
return `${words[0][0]}${words[1][0]}`.toUpperCase();
|
||||||
|
}
|
||||||
|
return name.substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProjectCard onClick={handleNavigate} key={id}>
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<CardHeader>
|
||||||
|
<ProjectId>ID: {id}</ProjectId>
|
||||||
|
<Stack direction={'row'} spacing={'.5rem'} sx={{
|
||||||
|
|
||||||
|
}} >
|
||||||
|
{!exportMode && (
|
||||||
|
<>
|
||||||
|
{status && (
|
||||||
|
<StatusChip
|
||||||
|
status={status}
|
||||||
|
label={getStatusLabel(status)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* {Пока нет редактирование + экспорт} */}
|
||||||
|
{/* <ExportButton onClick={handleExportProject} />
|
||||||
|
<Edit onClick={handleOpenEdit} /> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<ProjectTitle>{currentName}</ProjectTitle>
|
||||||
|
|
||||||
|
<ProjectInfoGrid>
|
||||||
|
{project_type && (
|
||||||
|
<InfoChip label={`Тип: ${project_type}`} size="small" />
|
||||||
|
)}
|
||||||
|
{report_count !== undefined && report_count !== null && (
|
||||||
|
<InfoChip label={`Отчетов: ${report_count}`} size="small" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
</ProjectInfoGrid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
direction: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardBody>
|
||||||
|
<IconWithContent icon={<DateIcon />}>
|
||||||
|
{years.join(', ') || 'Год не указан'}
|
||||||
|
</IconWithContent>
|
||||||
|
<IconWithContent icon={<UserIcon />}>
|
||||||
|
{org_unit_name || 'Организация не указана'}
|
||||||
|
</IconWithContent>
|
||||||
|
</CardBody>
|
||||||
|
|
||||||
|
{exportMode && (
|
||||||
|
<div>
|
||||||
|
{isChecked ? (
|
||||||
|
<CheckBoxCheckSvg size="1.625rem" />
|
||||||
|
) : (
|
||||||
|
<CheckBoxUncheckSvg size="1.625rem" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ProjectCard>
|
||||||
|
|
||||||
|
<ModalCreateProject
|
||||||
|
open={isEditModalOpen}
|
||||||
|
onClose={handleCloseEdit}
|
||||||
|
onCreate={handleSaveProject}
|
||||||
|
config={projectData}
|
||||||
|
initialData={editFormData}
|
||||||
|
isEdit={true}
|
||||||
|
projectId={id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
298
web/src/pages/ProjectsPage/ProjectsPage.jsx
Normal file
298
web/src/pages/ProjectsPage/ProjectsPage.jsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ProjectsApi } from '../../api/projects';
|
||||||
|
import { CardsSkeleton } from '../../components/common/Skeleton';
|
||||||
|
import SearchComponent from '../../components/common/SearchComponent';
|
||||||
|
import { ProjectCardComponent } from './ProjectCard';
|
||||||
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
|
import { PageContainer, TasksContainer } from '../StyledForPage';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { ModalCreateProject } from './ModalCreateProject';
|
||||||
|
import {
|
||||||
|
ROLES_NAME_ID,
|
||||||
|
} from '../../constants/constants';
|
||||||
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
|
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||||
|
import {
|
||||||
|
ExportExitButton,
|
||||||
|
ExportWithTextButton,
|
||||||
|
} from '../../components/common/Buttons/ButtonsActions';
|
||||||
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||||||
|
import { WhitePlus } from '../../components/common/icons/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { PaginatedList } from '../../components/common/PaginatedList';
|
||||||
|
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
||||||
|
import { SspApi } from '../../api/ssp';
|
||||||
|
import NavigationToggle from '../../components/common/NavigationToggle';
|
||||||
|
|
||||||
|
export { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Новые фильтры
|
||||||
|
const [yearFilter, setYearFilter] = useState('');
|
||||||
|
const [branchFilter, setBranchFilter] = useState('');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState(20);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [exportMode, setExportMode] = useState(false);
|
||||||
|
const [exportList, setExportList] = useState([]);
|
||||||
|
|
||||||
|
const [projectData, setProjectData] = useState(DEFAULT_PROJECT_CONFIG);
|
||||||
|
|
||||||
|
// Получаем список годов для фильтра (можно динамически или задать диапазон)
|
||||||
|
const yearOptions = useMemo(() => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = [];
|
||||||
|
for (let year = currentYear - 5; year <= currentYear + 2; year++) {
|
||||||
|
years.push(year);
|
||||||
|
}
|
||||||
|
return years;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
if (user.role_id == ROLES_NAME_ID.admin) {
|
||||||
|
SspApi.getAll().then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
setProjectData(prev => ({
|
||||||
|
...prev, 'ССП/РФ': {
|
||||||
|
...prev['ССП/РФ'],
|
||||||
|
options: data.result
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.error('Ошибка при получении ССП/РФ');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProjectData(prev => ({
|
||||||
|
...prev, 'ССП/РФ': {
|
||||||
|
...prev['ССП/РФ'],
|
||||||
|
options: user.org_units,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapFormDataToApi = (formData) => {
|
||||||
|
const apiData = {
|
||||||
|
name: formData['Название проекта'] || '',
|
||||||
|
project_type: formData['Тип проекта развития'] || null,
|
||||||
|
vsp_format: formData['Формат ВСП'] || null,
|
||||||
|
object_address: formData['Адрес объекта'] || '',
|
||||||
|
staff_count: formData['Целевая численность, чел.'] || 0,
|
||||||
|
placement_type: formData['Тип размещения'] || null,
|
||||||
|
total_area: formData['Площадь размещения, м2'] || 0,
|
||||||
|
year: formData['Год'] || null,
|
||||||
|
branch_id: formData['ССП/РФ'] || null,
|
||||||
|
|
||||||
|
};
|
||||||
|
return apiData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateProject = async (formData) => {
|
||||||
|
try {
|
||||||
|
const apiData = mapFormDataToApi(formData);
|
||||||
|
|
||||||
|
await ProjectsApi.create(apiData);
|
||||||
|
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, limit);
|
||||||
|
|
||||||
|
setModalOpen(false);
|
||||||
|
toast.success('Проект успешно создан');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
'Ошибка при создании проекта',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProjects = async (currentPage = page, currentLimit = limit) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const skip = (currentPage - 1) * currentLimit;
|
||||||
|
const params = {
|
||||||
|
offset: skip,
|
||||||
|
limit: currentLimit,
|
||||||
|
// Добавляем фильтры в параметры запроса
|
||||||
|
...(yearFilter && { year: parseInt(yearFilter) }),
|
||||||
|
...(branchFilter && { branch_id: parseInt(branchFilter.id) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await ProjectsApi.list(params);
|
||||||
|
const data = res.result;
|
||||||
|
|
||||||
|
setItems(data);
|
||||||
|
setTotalCount(res.count);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Ошибка загрузки проектов');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сбрасываем страницу при изменении любых фильтров
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, limit);
|
||||||
|
}, [yearFilter, branchFilter]);
|
||||||
|
|
||||||
|
const handlePageChange = (event, value) => {
|
||||||
|
setPage(value);
|
||||||
|
loadProjects(value, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLimitChange = (event) => {
|
||||||
|
const newLimit = event.target.value;
|
||||||
|
setLimit(newLimit);
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, newLimit);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сброс всех фильтров
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setYearFilter('');
|
||||||
|
setBranchFilter('');
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProjects(page, limit);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExportList([]);
|
||||||
|
}, [exportMode]);
|
||||||
|
|
||||||
|
const handleChangeToExportList = (projectId, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setExportList((prev) => [...prev, projectId]);
|
||||||
|
} else {
|
||||||
|
const newExportList = exportList.filter((t) => t != projectId);
|
||||||
|
setExportList(newExportList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkExport = async () => {
|
||||||
|
await exportBulkForms(exportList);
|
||||||
|
setExportMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSingleExport = (projectId, projectTitle) => {
|
||||||
|
exportSingleForm(projectId, projectTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loadProjects(page, limit);
|
||||||
|
toast.info('Список проектов обновлен');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContainer>
|
||||||
|
<HeaderSwitch />
|
||||||
|
<NavigationToggle currentView={'projects'} onViewChange={(path) => navigate(path)} />
|
||||||
|
|
||||||
|
<PaginatedList
|
||||||
|
items={items}
|
||||||
|
loading={loading}
|
||||||
|
totalCount={totalCount}
|
||||||
|
page={page}
|
||||||
|
limit={limit}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onLimitChange={handleLimitChange}
|
||||||
|
entityName="проектов"
|
||||||
|
emptyMessage="Проекты не найдены"
|
||||||
|
renderItem={(project, exportProps) => (
|
||||||
|
<ProjectCardComponent
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
projectDataInit={projectData}
|
||||||
|
{...exportProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
getItemId={(project) => project.id}
|
||||||
|
getItemTitle={(project) => project.name || 'Проект'}
|
||||||
|
extraActions={
|
||||||
|
user.role_id === ROLES_NAME_ID.admin && (
|
||||||
|
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} height='2.5rem' >
|
||||||
|
Создать проект
|
||||||
|
</PrimaryButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
filterActions={
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center', height: '2.5rem' }}>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
displayEmpty
|
||||||
|
value={yearFilter}
|
||||||
|
onChange={(e) => setYearFilter(e.target.value)}
|
||||||
|
sx={{ width: 130 }}
|
||||||
|
>
|
||||||
|
<MenuItem value="">Все годы</MenuItem>
|
||||||
|
{yearOptions.map((year) => (
|
||||||
|
<MenuItem key={year} value={year}>{year}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={projectData['ССП/РФ']?.options ?? []}
|
||||||
|
noOptionsText="Не найдено"
|
||||||
|
getOptionLabel={(option) => option.title || option.name || '-'}
|
||||||
|
value={branchFilter}
|
||||||
|
onChange={(event, newValue) => setBranchFilter(newValue)}
|
||||||
|
sx={{ width: 200 }}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} placeholder="Все" size="small" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<PrimaryOutlinedButton onClick={handleClearFilters} height="100%"> Сбросить</PrimaryOutlinedButton>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
|
||||||
|
<ModalCreateProject
|
||||||
|
open={modalOpen}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
onCreate={handleCreateProject}
|
||||||
|
config={projectData}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const PageContainer = styled.div`
|
const PageContainer = styled.div`
|
||||||
padding: 2.5rem;
|
padding: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export default function TablePage() {
|
|||||||
|
|
||||||
const connection = connectToTable(tableId, user?.id, {
|
const connection = connectToTable(tableId, user?.id, {
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error('Ошибка подключения к серверу реального времени');
|
toast.error('Ошибка подключения к серверу');
|
||||||
},
|
},
|
||||||
onClose: (event) => {
|
onClose: (event) => {
|
||||||
if (isUnmountingRef.current) {
|
if (isUnmountingRef.current) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { TasksApi } from '../../api/tasks';
|
import { TasksApi } from '../../api/tasks';
|
||||||
import { TablesApi } from '../../api/tables';
|
import { TablesApi } from '../../api/tables';
|
||||||
|
import { ProjectsApi } from '../../api/projects'; // Добавьте импорт API для проектов
|
||||||
import {
|
import {
|
||||||
TaskInfoContainer,
|
TaskInfoContainer,
|
||||||
NameTask,
|
NameTask,
|
||||||
@ -18,17 +19,16 @@ import { toast } from 'react-toastify';
|
|||||||
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
|
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
|
||||||
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||||
import { FormsApi } from '../../api/forms';
|
import { FormsApi } from '../../api/forms';
|
||||||
import { ModalCreateProject } from './ModalCreateProject';
|
|
||||||
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
||||||
import { exportSingleForm } from '../../utils/exportForm';
|
import { exportSingleForm } from '../../utils/exportFile';
|
||||||
import TablesList from '../../components/common/TableList/TableList';
|
import TablesList from '../../components/common/TableList/TableList';
|
||||||
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
|
import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants/constants';
|
||||||
|
|
||||||
const TaskInfo = ({ task }) => {
|
const TaskInfo = ({ task, isProject }) => {
|
||||||
const portalContent = (
|
const portalContent = (
|
||||||
<TaskInfoContainer>
|
<TaskInfoContainer>
|
||||||
<BackButton to="/tasks" />
|
<BackButton to={isProject ? "/projects" : "/tasks"} />
|
||||||
<NameTask>{FORM_TYPE_TRANSLATE[task.form_type_code] || ''}</NameTask>
|
<NameTask>{isProject ? 'Проект' : FORM_TYPE_TRANSLATE[task.form_type_code] || ''}</NameTask>
|
||||||
</TaskInfoContainer>
|
</TaskInfoContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -40,9 +40,13 @@ const TaskInfo = ({ task }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TaskPage() {
|
export default function TaskPage() {
|
||||||
const { taskId } = useParams();
|
const { taskId, projectId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const id = Number(taskId);
|
|
||||||
|
// Определяем, что у нас за ID и какой тип
|
||||||
|
const isProject = !!projectId;
|
||||||
|
const id = isProject ? Number(projectId) : Number(taskId);
|
||||||
|
|
||||||
const [task, setTask] = useState(null);
|
const [task, setTask] = useState(null);
|
||||||
const [tables, setTables] = useState([]);
|
const [tables, setTables] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -51,23 +55,42 @@ export default function TaskPage() {
|
|||||||
const [modalProjectOpen, setModalProjectOpen] = useState(false);
|
const [modalProjectOpen, setModalProjectOpen] = useState(false);
|
||||||
const [tempProjects, setTempProjects] = useState([]);
|
const [tempProjects, setTempProjects] = useState([]);
|
||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
|
const [isProjectData, setIsProjectData] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getSheets = async () => {
|
const getData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await TasksApi.getSheets(id);
|
if (isProject) {
|
||||||
// Преобразуем данные с учетом direction
|
// Получаем информацию о проекте
|
||||||
const mappedTables = data.result.all_sheets.map(sheet => ({
|
const projectData = await ProjectsApi.get(id);
|
||||||
name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name,
|
setTask(projectData.result);
|
||||||
sheet: sheet.sheet_name,
|
setIsProjectData(true);
|
||||||
direction: sheet.direction,
|
|
||||||
}));
|
const mappedTables = projectData.result.reports.map(sheet => ({
|
||||||
//пока скрыт
|
name: SHEET_NAME[sheet.report_type] || sheet.report_type,
|
||||||
setTables(mappedTables.filter(t => t.sheet !== "OTCH9F",));
|
sheet: sheet.report_type,
|
||||||
|
year: sheet.year,
|
||||||
|
}));
|
||||||
|
setTables(mappedTables);
|
||||||
|
} else {
|
||||||
|
// Получаем информацию о задаче
|
||||||
|
const taskData = await TasksApi.getById(id);
|
||||||
|
setTask(taskData.result);
|
||||||
|
setIsProjectData(false);
|
||||||
|
|
||||||
|
// Получаем таблицы задачи
|
||||||
|
const tablesData = await TasksApi.getSheets(id);
|
||||||
|
const mappedTables = tablesData.result.all_sheets.map(sheet => ({
|
||||||
|
name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name,
|
||||||
|
sheet: sheet.sheet_name,
|
||||||
|
direction: sheet.direction,
|
||||||
|
}));
|
||||||
|
setTables(mappedTables.filter(t => t.sheet !== "OTCH9F"));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
error?.response?.data?.detail || 'Ошибка получения данных задачи',
|
error?.response?.data?.detail || `Ошибка получения данных ${isProject ? 'проекта' : 'задачи'}`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -75,27 +98,11 @@ export default function TaskPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (Number.isFinite(id)) {
|
if (Number.isFinite(id)) {
|
||||||
getSheets();
|
getData();
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id, isProject]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Объединили два useEffect в один
|
||||||
const getFormInfo = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await TasksApi.getById(id);
|
|
||||||
setTask(data.result);
|
|
||||||
} catch (error) {
|
|
||||||
// обработка ошибки
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Number.isFinite(id)) {
|
|
||||||
getFormInfo();
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const handleCreateProject = (projectData) => {
|
const handleCreateProject = (projectData) => {
|
||||||
// логика создания проекта
|
// логика создания проекта
|
||||||
@ -107,6 +114,14 @@ export default function TaskPage() {
|
|||||||
return searchString.includes(query.toLowerCase());
|
return searchString.includes(query.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Функция для экспорта
|
||||||
|
const handleExport = () => {
|
||||||
|
const fileName = isProject
|
||||||
|
? `project_${task?.title || id}`
|
||||||
|
: `form_${task?.title || taskId}`;
|
||||||
|
exportSingleForm(id, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center pt-12">
|
<div className="flex justify-center pt-12">
|
||||||
@ -117,7 +132,7 @@ export default function TaskPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{task && <TaskInfo task={task} />}
|
{task && <TaskInfo task={task} isProject={isProject} />}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -143,9 +158,7 @@ export default function TaskPage() {
|
|||||||
onChange={setQuery}
|
onChange={setQuery}
|
||||||
/>
|
/>
|
||||||
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
|
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end', gap: '.5rem' }}>
|
||||||
<ExportWithTextButton
|
<ExportWithTextButton onClick={handleExport} />
|
||||||
onClick={() => exportSingleForm(taskId, task?.title ?? 'form')}
|
|
||||||
/>
|
|
||||||
<PrimaryOutlinedButton
|
<PrimaryOutlinedButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => setModalStageOpen(true)}
|
onClick={() => setModalStageOpen(true)}
|
||||||
@ -156,9 +169,10 @@ export default function TaskPage() {
|
|||||||
<SettingStageModal
|
<SettingStageModal
|
||||||
isOpen={modalStageOpen}
|
isOpen={modalStageOpen}
|
||||||
onClose={() => setModalStageOpen(false)}
|
onClose={() => setModalStageOpen(false)}
|
||||||
taskId={taskId}
|
taskId={isProject ? undefined : taskId}
|
||||||
mode="task"
|
projectId={isProject ? projectId : undefined}
|
||||||
formTypeCode={task?.form_type_code || ''}
|
mode={isProject ? "project" : "task"}
|
||||||
|
formTypeCode={ task?.form_type_code || ''}
|
||||||
sheets={tables}
|
sheets={tables}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -169,8 +183,9 @@ export default function TaskPage() {
|
|||||||
query={query}
|
query={query}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
filteredTables={filteredTables}
|
filteredTables={filteredTables}
|
||||||
basePath="/table"
|
basePath={"/table"}
|
||||||
formInfo={task}
|
formInfo={task}
|
||||||
|
isProject={isProject}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {
|
|||||||
ORG_UNIT_TYPE_OPTIONS,
|
ORG_UNIT_TYPE_OPTIONS,
|
||||||
ROLES_NAME_ID,
|
ROLES_NAME_ID,
|
||||||
FORM_TYPE_TRANSLATE
|
FORM_TYPE_TRANSLATE
|
||||||
} from '../../constants';
|
} from '../../constants/constants';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { SspApi } from '../../api/ssp';
|
import { SspApi } from '../../api/ssp';
|
||||||
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
||||||
@ -35,10 +35,13 @@ import {
|
|||||||
ExportExitButton,
|
ExportExitButton,
|
||||||
ExportWithTextButton,
|
ExportWithTextButton,
|
||||||
} from '../../components/common/Buttons/ButtonsActions';
|
} from '../../components/common/Buttons/ButtonsActions';
|
||||||
import { exportBulkForms, exportSingleForm } from '../../utils/exportForm';
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||||||
import { WhitePlus } from '../../components/common/icons/icons';
|
import { WhitePlus } from '../../components/common/icons/icons';
|
||||||
import { OrgUnitsAutocompletePaper } from './OrgUnitsAutocompletePaper';
|
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
|
||||||
import { renderOrgUnitValue } from './renderOrgUnitValue';
|
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
|
||||||
|
import { PaginatedList } from '../../components/common/PaginatedList';
|
||||||
|
import NavigationToggle from '../../components/common/NavigationToggle';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
const modalSelectMenuProps = {
|
const modalSelectMenuProps = {
|
||||||
sx: { zIndex: 1301 },
|
sx: { zIndex: 1301 },
|
||||||
@ -52,6 +55,8 @@ const modalSelectSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TasksPage() {
|
export default function TasksPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||||
@ -255,128 +260,34 @@ export default function TasksPage() {
|
|||||||
<>
|
<>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<HeaderSwitch />
|
<HeaderSwitch />
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<NavigationToggle currentView={'tasks'} onViewChange={(path) => navigate(path)} />
|
||||||
{exportMode && <span>Выделено задач: {exportList.length}</span>}
|
<PaginatedList
|
||||||
</Box>
|
items={items}
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
loading={loading}
|
||||||
<div style={{ width: '20rem' }}>
|
totalCount={totalCount}
|
||||||
{/* <SearchComponent
|
page={page}
|
||||||
placeholder="Поиск по названию задачи"
|
limit={limit}
|
||||||
value={query}
|
onPageChange={handlePageChange}
|
||||||
onChange={setQuery}
|
onLimitChange={handleLimitChange}
|
||||||
/> */}
|
entityName="задач"
|
||||||
</div>
|
emptyMessage="Задачи не найдены"
|
||||||
<Stack direction={'row'} spacing={'.5rem'}>
|
renderItem={(task, exportProps) => (
|
||||||
{!exportMode ? (
|
<TaskCardComponent
|
||||||
<>
|
key={task.id}
|
||||||
<ExportWithTextButton
|
taskForm={task}
|
||||||
text={'Пакетный экспорт'}
|
{...exportProps}
|
||||||
onClick={() => {
|
|
||||||
setExportMode(true);
|
|
||||||
}}
|
|
||||||
></ExportWithTextButton>
|
|
||||||
{user.role_id == ROLES_NAME_ID.admin && (
|
|
||||||
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />}>
|
|
||||||
Создать задачу
|
|
||||||
</PrimaryButton>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExportWithTextButton
|
|
||||||
text={'Экспортировать'}
|
|
||||||
onClick={handleBulkExport}
|
|
||||||
disabled={exportList.length === 0}
|
|
||||||
/>
|
|
||||||
<ExportExitButton onClick={() => setExportMode(false)} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* TODO: вернуть, когда будет реализовано на бэке */}
|
|
||||||
{/* <FilterComponent statusFilter={statusFilter} setStatusFilter={setStatusFilter} /> */}
|
|
||||||
<TasksContainer>
|
|
||||||
{loading ? (
|
|
||||||
<CardsSkeleton count={limit} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{items.map((t) => (
|
|
||||||
<TaskCardComponent
|
|
||||||
exportMode={exportMode}
|
|
||||||
taskForm={t}
|
|
||||||
key={t.id + '-task-id'}
|
|
||||||
onAddForExport={(checked) => {
|
|
||||||
handleChangeToExportList(t.id, checked);
|
|
||||||
}}
|
|
||||||
onExportClick={handleSingleExport}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TasksContainer>
|
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
|
||||||
{!loading && items.length > 0 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
pt: 2,
|
|
||||||
pb: 2,
|
|
||||||
borderTop: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
Показать на странице:
|
|
||||||
</Typography>
|
|
||||||
<Select
|
|
||||||
value={limit}
|
|
||||||
onChange={handleLimitChange}
|
|
||||||
size="small"
|
|
||||||
sx={{ minWidth: 70 }}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<MenuItem value={10}>10</MenuItem>
|
|
||||||
<MenuItem value={20}>20</MenuItem>
|
|
||||||
<MenuItem value={50}>50</MenuItem>
|
|
||||||
<MenuItem value={100}>100</MenuItem>
|
|
||||||
<MenuItem value={200}>200</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Pagination
|
|
||||||
count={Math.ceil(totalCount / limit)}
|
|
||||||
page={page}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
showFirstButton
|
|
||||||
showLastButton
|
|
||||||
disabled={loading}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<PaginationItem
|
|
||||||
{...item}
|
|
||||||
sx={{
|
|
||||||
'&.Mui-selected': {
|
|
||||||
backgroundColor: 'primary.main',
|
|
||||||
color: 'white',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'primary.dark',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Box sx={{ width: 120 }} /> {/* Spacer for symmetry */}
|
)}
|
||||||
</Box>
|
getItemId={(task) => task.id}
|
||||||
)}
|
getItemTitle={(task) => task.title || 'Задача'}
|
||||||
|
extraActions={
|
||||||
|
user.role_id === ROLES_NAME_ID.admin && (
|
||||||
|
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} height={'2.5rem'}>
|
||||||
|
Создать задачу
|
||||||
|
</PrimaryButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
<Modal
|
<Modal
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
64
web/src/utils/exportFile.js
Normal file
64
web/src/utils/exportFile.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { FormsSheetApi } from "../api/form_sheet";
|
||||||
|
import { ProjectsApi } from "../api/projects";
|
||||||
|
import { TasksApi } from "../api/tasks";
|
||||||
|
import { downloadFile } from "./downloadFile";
|
||||||
|
import { getApiErrorMessage } from "./getApiErrorMessage";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const exportSingleForm = async (taskId, taskTitle) => {
|
||||||
|
try {
|
||||||
|
const response = await TasksApi.exportTask(taskId);
|
||||||
|
await downloadFile(response, taskTitle, "xlsx");
|
||||||
|
} catch (error) {
|
||||||
|
const message = await getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"Ошибка при скачивании файла",
|
||||||
|
);
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportSheet = async (formId, sheetName, direction) => {
|
||||||
|
try {
|
||||||
|
const normalizedDirection =
|
||||||
|
direction && direction !== "null" ? direction : undefined;
|
||||||
|
const params = normalizedDirection
|
||||||
|
? { direction: normalizedDirection }
|
||||||
|
: {};
|
||||||
|
const response = await FormsSheetApi.export(formId, sheetName, params);
|
||||||
|
await downloadFile(response, sheetName, "xlsx");
|
||||||
|
} catch (error) {
|
||||||
|
const message = await getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"Ошибка при скачивании файла",
|
||||||
|
);
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportSheetProject = async (formId, sheetName, year) => {
|
||||||
|
try {
|
||||||
|
const response = await ProjectsApi.export(formId, year, sheetName);
|
||||||
|
await downloadFile(response, sheetName, "xlsx");
|
||||||
|
} catch (error) {
|
||||||
|
const message = await getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"Ошибка при скачивании файла",
|
||||||
|
);
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportBulkForms = async (formIds, fileName = "Задачи") => {
|
||||||
|
try {
|
||||||
|
const response = await TasksApi.exportTasks({ form_ids: formIds });
|
||||||
|
await downloadFile(response, fileName, "xlsx");
|
||||||
|
} catch (error) {
|
||||||
|
const message = await getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
"Ошибка при скачивании файла",
|
||||||
|
);
|
||||||
|
toast.error(message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { FormsSheetApi } from '../api/form_sheet';
|
|
||||||
import { TasksApi } from '../api/tasks';
|
|
||||||
import { downloadFile } from './downloadFile';
|
|
||||||
import { getApiErrorMessage } from './getApiErrorMessage';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
export const exportSingleForm = async (taskId, taskTitle) => {
|
|
||||||
try {
|
|
||||||
const response = await TasksApi.exportTask(taskId);
|
|
||||||
await downloadFile(response, taskTitle, 'xlsx');
|
|
||||||
} catch (error) {
|
|
||||||
const message = await getApiErrorMessage(
|
|
||||||
error,
|
|
||||||
'Ошибка при скачивании файла',
|
|
||||||
);
|
|
||||||
toast.error(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const exportSheet = async (formId, sheetName, direction) => {
|
|
||||||
try {
|
|
||||||
const normalizedDirection =
|
|
||||||
direction && direction !== 'null' ? direction : undefined;
|
|
||||||
const params = normalizedDirection ? { direction: normalizedDirection } : {};
|
|
||||||
const response = await FormsSheetApi.export(formId, sheetName, params);
|
|
||||||
await downloadFile(response, sheetName, 'xlsx');
|
|
||||||
} catch (error) {
|
|
||||||
const message = await getApiErrorMessage(
|
|
||||||
error,
|
|
||||||
'Ошибка при скачивании файла',
|
|
||||||
);
|
|
||||||
toast.error(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const exportBulkForms = async (formIds, fileName = 'Задачи') => {
|
|
||||||
try {
|
|
||||||
const response = await TasksApi.exportTasks({ form_ids: formIds });
|
|
||||||
await downloadFile(response, fileName, 'xlsx');
|
|
||||||
} catch (error) {
|
|
||||||
const message = await getApiErrorMessage(
|
|
||||||
error,
|
|
||||||
'Ошибка при скачивании файла',
|
|
||||||
);
|
|
||||||
toast.error(message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user