Compare commits

..

2 Commits

14 changed files with 307 additions and 26 deletions

View File

@ -0,0 +1,84 @@
import os
import re
import time
from alembic import op
import sqlalchemy as sa
revision = "0011"
down_revision = "0010"
branch_labels = None
depends_on = None
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
def _find_dollar_tag(line: str) -> str | None:
m = _DOLLAR_TAG_RE.search(line.strip())
return m.group(0) if m else None
def _split_statements(sql: str) -> list[str]:
statements: list[str] = []
current: list[str] = []
in_dollar = False
dollar_tag: str | None = None
for line in sql.split("\n"):
stripped = line.strip()
if stripped.startswith("--"):
continue
if not in_dollar:
tag = _find_dollar_tag(stripped)
if tag and tag.endswith("$") and tag.startswith("$"):
dollar_tag = tag
in_dollar = True
current.append(line)
continue
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
after = stripped[len(dollar_tag):].strip()
if after == ";" or after == "":
in_dollar = False
dollar_tag = None
if after == ";":
current.append(line)
statements.append("\n".join(current))
current = []
continue
if not in_dollar and stripped.rstrip().endswith(";"):
current.append(line)
statements.append("\n".join(current))
current = []
continue
current.append(line)
remaining = "\n".join(current).strip()
if remaining:
statements.append(remaining)
return statements
def upgrade() -> None:
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0011_form3_log.sql")
with open(ddl_path) as f:
content = f.read()
statements = _split_statements(content)
for stmt in statements:
stripped = stmt.strip().rstrip(";").strip()
if not stripped:
continue
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
continue
op.execute(stripped)
def downgrade() -> None:
pass

View File

@ -0,0 +1,172 @@
CREATE OR REPLACE FUNCTION v3.add_project_year(p_project_id integer, p_year integer, p_emit_audit boolean DEFAULT true)
RETURNS TABLE(limit_report_id integer, current_expenses_report_id integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_lim INT;
v_cur INT;
BEGIN
IF p_year IS NULL THEN
RAISE EXCEPTION 'p_year обязателен';
END IF;
IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
RAISE EXCEPTION 'project id=% не существует', p_project_id;
END IF;
IF EXISTS (SELECT 1 FROM v3.rf_project_report
WHERE project_id = p_project_id AND year = p_year) THEN
RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
END IF;
INSERT INTO v3.rf_project_report (project_id, year, report_type)
VALUES (p_project_id, p_year, 'LIMIT') RETURNING id INTO v_lim;
INSERT INTO v3.rf_project_report (project_id, year, report_type)
VALUES (p_project_id, 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.seed_form3_report_lines(v_lim);
PERFORM v3.seed_form3_report_lines(v_cur);
IF p_emit_audit THEN
PERFORM v3.log_event(
'PROJECT_YEAR_ADD', 'PROJECT',
jsonb_build_object(
'project_id', p_project_id,
'year', p_year,
'limit_report_id', v_lim,
'current_expenses_report_id', v_cur
)
);
END IF;
RETURN QUERY SELECT v_lim, v_cur;
END;
$function$
;
-- FKs
ALTER TABLE v3.app_user ADD CONSTRAINT app_user_role_fk FOREIGN KEY (role_id) REFERENCES v3."role"(id);
ALTER TABLE v3.audit_log ADD CONSTRAINT audit_log_app_user_fk FOREIGN KEY (user_id) REFERENCES v3.app_user(id);
ALTER TABLE v3.audit_log ADD CONSTRAINT audit_log_org_unit_fk FOREIGN KEY (org_unit_id) REFERENCES v3.org_unit(id);
ALTER TABLE v3.audit_log ADD CONSTRAINT audit_log_budget_form_fk FOREIGN KEY (form_id) REFERENCES v3.budget_form(id);
ALTER TABLE v3.booking ADD CONSTRAINT booking_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.budget_form ADD CONSTRAINT budget_form_app_user_fk FOREIGN KEY (created_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.budget_form ADD CONSTRAINT budget_form_app_user_fk_1 FOREIGN KEY (updated_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.budget_form ADD CONSTRAINT budget_form_org_unit_fk FOREIGN KEY (org_unit_id) REFERENCES v3.org_unit(id);
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_budget_form_fk FOREIGN KEY (budget_form_id) REFERENCES v3.budget_form(id);
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_expense_item_fk FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_vsp_fk FOREIGN KEY (vsp_id) REFERENCES v3.vsp(id);
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_app_user_fk FOREIGN KEY (created_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_app_user_fk_1 FOREIGN KEY (updated_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.budget_line_quarter ADD CONSTRAINT budget_line_quarter_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.ckk ADD CONSTRAINT ckk_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.collegial_approval ADD CONSTRAINT collegial_approval_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.contract_detail ADD CONSTRAINT contract_detail_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.contract_summary ADD CONSTRAINT contract_summary_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.expense_item ADD CONSTRAINT expense_item_expense_item_fk FOREIGN KEY (parent_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.expense_item_form_type ADD CONSTRAINT expense_item_form_type_expense_item_fk FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.fixed_asset_report ADD CONSTRAINT fixed_asset_report_budget_form_fk FOREIGN KEY (budget_form_id) REFERENCES v3.budget_form(id);
ALTER TABLE v3.fixed_asset_report ADD CONSTRAINT fixed_asset_report_expense_item_fk FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.form3_phase ADD CONSTRAINT form3_phase_rf_project_report_fk FOREIGN KEY (rf_project_report_id) REFERENCES v3.rf_project_report(id);
ALTER TABLE v3.form_limit ADD CONSTRAINT form_limit_budget_form_fk FOREIGN KEY (budget_form_id) REFERENCES v3.budget_form(id);
ALTER TABLE v3.form_phase ADD CONSTRAINT form_phase_budget_form_fk FOREIGN KEY (budget_form_id) REFERENCES v3.budget_form(id);
ALTER TABLE v3.phase_template ADD CONSTRAINT phase_template_form_type_fk FOREIGN KEY (form_type) REFERENCES v3.form_type(code);
ALTER TABLE v3."plan" ADD CONSTRAINT plan_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.project ADD CONSTRAINT project_org_unit_fk FOREIGN KEY (org_unit_id) REFERENCES v3.org_unit(id);
ALTER TABLE v3.rent_detail ADD CONSTRAINT rent_detail_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.rent_detail ADD CONSTRAINT rent_detail_vsp_fk FOREIGN KEY (vsp_id) REFERENCES v3.vsp(id);
ALTER TABLE v3.reserve ADD CONSTRAINT reserve_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.rf_project_report ADD CONSTRAINT rf_project_report_project_fk FOREIGN KEY (project_id) REFERENCES v3.project(id);
ALTER TABLE v3.rf_project_report ADD CONSTRAINT rf_project_report_app_user_fk FOREIGN KEY (created_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.rf_project_report ADD CONSTRAINT rf_project_report_app_user_fk_1 FOREIGN KEY (updated_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.rf_project_report_line ADD CONSTRAINT rf_project_report_line_rf_project_report_fk FOREIGN KEY (rf_project_report_id) REFERENCES v3.rf_project_report(id);
ALTER TABLE v3.rf_project_report_line ADD CONSTRAINT rf_project_report_line_expense_item_fk FOREIGN KEY (expense_item_id) REFERENCES v3.expense_item(id);
ALTER TABLE v3.rf_project_report_quarter ADD CONSTRAINT rf_project_report_quarter_rf_project_report_line_fk FOREIGN KEY (rf_project_report_line_id) REFERENCES v3.rf_project_report_line(id);
ALTER TABLE v3.security_detail ADD CONSTRAINT security_detail_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.security_detail ADD CONSTRAINT security_detail_vsp_fk FOREIGN KEY (vsp_id) REFERENCES v3.vsp(id);
ALTER TABLE v3.sequestration ADD CONSTRAINT sequestration_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.user_org ADD CONSTRAINT user_org_app_user_fk FOREIGN KEY (user_id) REFERENCES v3.app_user(id);
ALTER TABLE v3.user_org ADD CONSTRAINT user_org_org_unit_fk FOREIGN KEY (org_unit_id) REFERENCES v3.org_unit(id);
ALTER TABLE v3.utility_detail ADD CONSTRAINT utility_detail_budget_line_fk FOREIGN KEY (line_id) REFERENCES v3.budget_line(id);
ALTER TABLE v3.utility_detail ADD CONSTRAINT utility_detail_vsp_fk FOREIGN KEY (vsp_id) REFERENCES v3.vsp(id);
ALTER TABLE v3.vsp ADD CONSTRAINT vsp_org_unit_fk FOREIGN KEY (branch_id) REFERENCES v3.org_unit(id);
ALTER TABLE v3.vsp ADD CONSTRAINT vsp_app_user_fk FOREIGN KEY (created_by) REFERENCES v3.app_user(id);
ALTER TABLE v3.vsp ADD CONSTRAINT vsp_app_user_fk_1 FOREIGN KEY (updated_by) REFERENCES v3.app_user(id);
-- DROP FUNCTION v3.add_project(varchar, int4, int4, varchar, int4, varchar, varchar, varchar, varchar, int4, numeric, varchar);
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
LANGUAGE plpgsql
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 ou WHERE id = p_org_unit_id AND ou.is_active ) 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 project (CHECK-ы name/project_type/vsp_format/placement_type
-- из migrate_form3_v1.sql применятся автоматически).
INSERT INTO v3.project (
name, level, parent_id, org_unit_id,
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
system_code
) 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,
p_system_code
) RETURNING id INTO v_pid;
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
-- годы добавляются отдельно через v3.add_project_year.
SELECT y.limit_report_id, y.current_expenses_report_id
INTO v_lim, v_cur
FROM v3.add_project_year(v_pid, p_year, false) y;
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
PERFORM v3.log_event(
'PROJECT_CREATE', 'PROJECT',
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,
'system_code', p_system_code,
'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$
;

View File

@ -46,6 +46,7 @@ async def get_vsp(
staff_count_min=staff_count_min,
staff_count_max=staff_count_max,
load_org_unit=True,
is_active=True,
with_count=True,
)
return BaseListResponse(

View File

@ -33,6 +33,7 @@ class Vsp(Base):
total_area: Mapped[float | None] = mapped_column(Numeric)
closed_at: Mapped[date | None] = mapped_column(Date)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
#ToDo: а нужно ли нам и is_active и is_deleted?
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
system_code: Mapped[str | None] = mapped_column(String(100), nullable=True)
created_at: Mapped[datetime] = mapped_column(

View File

@ -14,8 +14,14 @@ class VSPRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get(self, vsp_id: int, is_active: bool | None = None, load_org_unit: bool = False) -> Optional[Vsp]:
queries = [Vsp.id == vsp_id, Vsp.is_deleted.is_(False)]
async def get(
self,
vsp_id: int,
is_active: bool | None = None,
load_org_unit: bool = False,
with_deleted: bool = False,
) -> Optional[Vsp]:
queries = [Vsp.id == vsp_id, Vsp.is_deleted.is_(with_deleted)]
if is_active is not None:
queries.append(Vsp.is_active == is_active)
if load_org_unit:

View File

@ -49,6 +49,7 @@ SHEET_NAMES = {
'AHR_SECURITY': 'АХР Безопасность',
'AHR_UTILITY': 'АХР Коммунальные услуги',
'SMETA': 'Смета',
'STRUCTURE': 'Структура',
}
SKIP_SHEETS = ('OTCH9F',)
@ -327,8 +328,8 @@ class ExportService:
("Форма ID", form.id),
("Тип формы", form.form_type_code),
("Год", form.year),
("Org Unit ID", form.org_unit_id),
("Org Unit", form.org_unit.title if form.org_unit else None),
("ID ССП.РФ", form.org_unit_id),
("ССП/РФ", form.org_unit.title if form.org_unit else None),
("Экспортировано", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")),
]
for row_idx, (key, value) in enumerate(metadata_rows, start=1):

View File

@ -36,7 +36,11 @@ class VSPService:
self.user_repo = UserRepository(db)
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
vsp = await self.vsp_repo.get(vsp_id=vsp_id, load_org_unit=load_org_unit)
vsp = await self.vsp_repo.get(
vsp_id=vsp_id,
load_org_unit=load_org_unit,
with_deleted=user.role_id == UserRoleEnum.ADMIN,
)
if not vsp:
return None
if user.role_id == UserRoleEnum.ADMIN:
@ -173,7 +177,7 @@ class VSPService:
placement_type=placement_type,
staff_count_min=staff_count_min,
staff_count_max=staff_count_max,
is_active=None,
is_active=True,
load_org_unit=True,
)

View File

@ -58,8 +58,8 @@ def test_vsp_dropdown_with_filter(client, admin_tokens, auth_headers, params, co
assert len(payload["result"]) == count
def test_vsp_get_by_id(client, admin_tokens, auth_headers):
response = client.get("/api/v1/dict/info/1", headers=auth_headers(admin_tokens))
def test_vsp_get_by_id(client, isp_tokens, auth_headers):
response = client.get("/api/v1/dict/info/1", headers=auth_headers(isp_tokens))
assert response.status_code == 200
payload = response.json()
assert "result" in payload

View File

@ -28,9 +28,9 @@ INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,op
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05');
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),
(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','2027-10-02'),
(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','2027-07-01');
(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',1,'','','',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',1,'123-1234',1,'субаренда','Работает ','12-45','2027-10-02'),
(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',1,'3434',NULL,'2','примечание','567','2027-07-01');
SELECT setval('v3.vsp_id_seq', 3);
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id) VALUES

View File

@ -22,7 +22,7 @@ import { CalendarInput } from './Calendar/CalendarInput';
import { Cancel } from '../common/icons/icons';
import { getStageStatus, getStatusColors } from './StagesTable/utils';
import { StageStatusChip } from './StageStatusChip';
import { formatDateForApi } from './utils';
import { formatDateForApi } from '../../utils/formatDate';
import CollapsibleTree from '../common/CollapsibleTree';
import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils';
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants/constants';

View File

@ -1,11 +1,3 @@
export const formatDateForApi = (value) => {
if (!value) return '';
const d = new Date(value);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
export const isPastDate = (value) => {
if (!value) return false;
const date = new Date(value);
@ -14,4 +6,3 @@ export const isPastDate = (value) => {
today.setHours(0, 0, 0, 0);
return date < today;
};

View File

@ -12,6 +12,7 @@ import { UsersApi } from '../../../api/users';
import AuditLogsFilters from './components/AuditLogsFilters';
import AuditLogsTable from './components/AuditLogsTable';
import { DEFAULT_ACTIONS } from './constants/auditLogs.jsx';
import {formatDateForApi} from '../../../utils/formatDate.js'
const AuditLogsPage = () => {
const { user } = useAuth();
@ -71,12 +72,13 @@ const AuditLogsPage = () => {
user_id: selectedUser?.id,
org_unit_id: selectedSsp?.id,
event: selectedEventType?.id,
date_from: dateFrom ? dateFrom.toISOString().slice(0, 10) : undefined,
date_to: dateTo ? dateTo.toISOString().slice(0, 10) : undefined,
// date_from: dateFrom ? new Date(dateFrom.getTime() - (dateFrom.getTimezoneOffset() * 60 * 1000)).toISOString().slice(0, 10): undefined,
// date_to: dateTo ? new Date(dateTo.getTime() - (dateTo.getTimezoneOffset() * 60 * 1000)).toISOString().slice(0, 10) : undefined,
date_from: dateFrom ? formatDateForApi(dateFrom) : undefined,
date_to: dateTo ? formatDateForApi(dateTo) : undefined,
task_id: taskId ? taskId : undefined,
form_id: formId ? formId : undefined,
};
const data = await AuditLogsApi.getAll(params);
setLogs(data.result ?? []);
setTotalCount(data.count ?? 0);

View File

@ -76,7 +76,7 @@ export default function ProjectsPage() {
useEffect(() => {
if (!user) return;
if (user.role_id == ROLES_NAME_ID.admin) {
SspApi.getAll().then(data => {
SspApi.getAll({is_active: true}).then(data => {
if (data.success) {
setProjectData(prev => ({
...prev, 'ССП/РФ': {

View File

@ -5,3 +5,22 @@ export const formatDate = (dateString) => {
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
export const formatDateForApi = (value) => {
if (!value) return '';
const d = new Date(value);
const year = String(d.getFullYear()).padStart(4, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
export const isPastDate = (value) => {
if (!value) return false;
const date = new Date(value);
date.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};